Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1349789226 | ||
|
|
9cbd7fb97d | ||
|
|
2afbc5c9c7 | ||
|
|
3558647e8d | ||
|
|
d954c83b78 | ||
|
|
9a01426864 | ||
|
|
ed2fd86f6b | ||
|
|
528a2597ed | ||
|
|
e431de8b42 | ||
|
|
7691cab3bc | ||
|
|
91c1857368 | ||
|
|
6e302db62e | ||
|
|
1f2990e4da |
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Opportunity;
|
||||
use App\Services\OfferAvailability;
|
||||
use App\Services\PrivateOfferOperations;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
final class ExpirePrivateOffers extends Command
|
||||
{
|
||||
protected $signature = 'investmoz:offers-expire {--confirm : Persist the already effective expiry}';
|
||||
|
||||
protected $description = 'Conferir expiração de Ofertas sem publicar conteúdos ou alterar o NDA';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$count = 0;
|
||||
Opportunity::query()->whereNotNull('workflow_context->private_offer_preparation')->orderBy('id')->chunkById(100, function ($records) use (&$count) {
|
||||
foreach ($records as $record) {
|
||||
if (app(OfferAvailability::class)->record($record)['state'] !== 'expired') {
|
||||
continue;
|
||||
}
|
||||
if ($this->option('confirm')) {
|
||||
if (app(PrivateOfferOperations::class)->expire($record->id)) {
|
||||
$count++;
|
||||
}
|
||||
} else {
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
});
|
||||
$this->info(($this->option('confirm') ? 'Expirações registadas: ' : 'Expiradas, sem escrita: ').$count);
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Services\InvestmentGeography;
|
||||
use App\Services\OfferPublicCatalogue;
|
||||
use App\Services\OpportunityManagement;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
final class OfferPublicCatalogueController extends Controller
|
||||
{
|
||||
public function index(Request $request, OfferPublicCatalogue $catalogue): Response
|
||||
{
|
||||
$filters = $catalogue->filters($request);
|
||||
|
||||
return $this->uncached(Inertia::render('offers/catalogue', [...$catalogue->page($request, $filters), 'filters' => $filters,
|
||||
'mapData' => in_array($filters['view'] ?? '', ['map', 'combined'], true) ? $catalogue->map($filters) : null,
|
||||
'territorySelection' => app(InvestmentGeography::class)->selection($filters),
|
||||
'references' => ['categories' => OpportunityManagement::CATEGORIES, 'options' => [], 'clusters' => [], 'corridors' => [], 'projects' => [], 'locations' => [], 'sectors' => []]])->toResponse($request));
|
||||
}
|
||||
|
||||
public function show(Request $request, int $id, OfferPublicCatalogue $catalogue): Response
|
||||
{
|
||||
return $this->uncached(Inertia::render('offers/public-detail', ['offer' => $catalogue->detail($id)])->toResponse($request));
|
||||
}
|
||||
|
||||
public function search(Request $request, OfferPublicCatalogue $catalogue): JsonResponse
|
||||
{
|
||||
return response()->json($catalogue->page($request, $catalogue->filters($request)))->header('Cache-Control', 'private, no-store');
|
||||
}
|
||||
|
||||
public function detail(int $id, OfferPublicCatalogue $catalogue): JsonResponse
|
||||
{
|
||||
return response()->json(['data' => $catalogue->detail($id)])->header('Cache-Control', 'private, no-store');
|
||||
}
|
||||
|
||||
public function map(Request $request, OfferPublicCatalogue $catalogue): JsonResponse
|
||||
{
|
||||
return response()->json($catalogue->map($catalogue->filters($request)))->header('Cache-Control', 'private, no-store');
|
||||
}
|
||||
|
||||
public function export(Request $request, OfferPublicCatalogue $catalogue): StreamedResponse
|
||||
{
|
||||
$result = $catalogue->records($catalogue->filters($request));
|
||||
|
||||
return response()->streamDownload(function () use ($result) {
|
||||
$stream = fopen('php://output', 'w');
|
||||
if ($stream === false) {
|
||||
return;
|
||||
}
|
||||
fwrite($stream, "\xEF\xBB\xBF");
|
||||
fputcsv($stream, ['Título', 'Categoria', 'Localização', 'Preço', 'Moeda', 'Publicação', 'URL'], ';', '"', '');
|
||||
foreach ($result['records'] as $record) {
|
||||
$fields = [$record['title'], $record['category'], implode(' / ', array_column($record['location'], 'name')),
|
||||
$record['price']['mode'] === 'negotiable' ? 'A negociar' : $record['price']['amount'], $record['price']['currency'], $record['published_at'], url($record['url'])];
|
||||
fputcsv($stream, array_map(fn ($value) => preg_match('/^[\s]*[=+@-]/u', (string) $value) ? "'".$value : $value, $fields), ';', '"', '');
|
||||
}
|
||||
fclose($stream);
|
||||
}, 'ofertas.csv', ['Content-Type' => 'text/csv; charset=UTF-8', 'Cache-Control' => 'private, no-store', 'X-Export-Limit' => '1000', 'X-Results-Truncated' => $result['truncated'] ? 'true' : 'false']);
|
||||
}
|
||||
|
||||
private function uncached(Response $response): Response
|
||||
{
|
||||
$response->headers->set('Cache-Control', 'private, no-store');
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
@@ -105,7 +105,10 @@ final class PortalEditorialWorkflowController extends Controller
|
||||
}
|
||||
$items = $query->orderByDesc('e.updated_at')->paginate(25, ['r.id', DB::raw("e.payload->'resource'->>'title' AS title"), 'r.document_type', 'r.published', 'e.id as edition_id', 'e.revision_no', 'e.state', 'e.legal_required', 'u.name as responsible'])->withQueryString();
|
||||
$destinations = app(ContentManagement::class)->access($actor);
|
||||
$data = ['items' => $items, 'filters' => $filters, 'notices' => $workflow->notices($actor), 'resourceAccess' => $access->full($actor) || $access->editorial($actor), 'opportunitiesUrl' => $destinations['opportunities']['index'] ?? null, 'opportunityPending' => $workflow->opportunityPending($actor), 'clustersUrl' => $destinations['clusters']['index'] ?? null,
|
||||
$data = ['items' => $items, 'filters' => $filters, 'notices' => $workflow->notices($actor), 'resourceAccess' => $access->full($actor) || $access->editorial($actor), 'opportunitiesUrl' => $destinations['opportunities']['index'] ?? null,
|
||||
'offersUrl' => isset($destinations['opportunities']['index']) && app(PortalProfileAccess::class)->available() ? '/minhas-oportunidades/ofertas' : null,
|
||||
'offerOperationsUrl' => ! app(PortalProfileAccess::class)->has($actor, 'auditor') && (app(PortalProfileAccess::class)->has($actor, 'validator') || app(PortalProfileAccess::class)->has($actor, 'publisher')) ? '/minhas-oportunidades/ofertas?operational=1' : null,
|
||||
'opportunityPending' => $workflow->opportunityPending($actor), 'clustersUrl' => $destinations['clusters']['index'] ?? null,
|
||||
'portfolioUrl' => $destinations['portfolio']['index'] ?? null, 'portfolioPending' => $workflow->portfolioPending($actor), 'demographyUrl' => $destinations['demography']['index'] ?? null, 'demographyPending' => $workflow->demographyPending($actor),
|
||||
'territorialUrl' => app(TerritorialRegistry::class)->editorialAvailable() && $actor->can('manage-portal') ? '/management/territorial/editorial' : null,
|
||||
'nativePending' => app(PortalEditorialNativeNotices::class)->pending($actor), 'zonesUrl' => app(PortalEditorialNativeNotices::class)->access($actor, 'investment-zones') ? '/management/investment/zones' : null,
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Opportunity;
|
||||
use App\Services\PrivateOfferOperations;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
final class PrivateOfferOperationsController extends Controller
|
||||
{
|
||||
public function show(Request $request, int $id, PrivateOfferOperations $operations): Response
|
||||
{
|
||||
return response()->json($operations->presentation($request->user(), $id));
|
||||
}
|
||||
|
||||
public function owner(Request $request, int $id, PrivateOfferOperations $operations): Response
|
||||
{
|
||||
$operations->ownerAction($request->user(), $id, $request->all());
|
||||
|
||||
return response()->json($operations->presentation($request->user(), $id));
|
||||
}
|
||||
|
||||
public function reportPage(Request $request, int $id, PrivateOfferOperations $operations): Response
|
||||
{
|
||||
$record = Opportunity::findOrFail($id);
|
||||
abort_unless($record->isPrivateOfferPreparation() && $operations->canReport($request->user(), $record), 404);
|
||||
|
||||
return Inertia::render('offers/report', ['offerId' => $id])->toResponse($request);
|
||||
}
|
||||
|
||||
public function report(Request $request, int $id, PrivateOfferOperations $operations): Response
|
||||
{
|
||||
return response()->json($operations->report($request->user(), $id, $request->all()), 201);
|
||||
}
|
||||
|
||||
public function handleReport(Request $request, int $id, string $reportId, PrivateOfferOperations $operations): Response
|
||||
{
|
||||
$operations->handleReport($request->user(), $id, $reportId, $request->all());
|
||||
|
||||
return response()->json($operations->presentation($request->user(), $id));
|
||||
}
|
||||
|
||||
public function highlight(Request $request, int $id, PrivateOfferOperations $operations): Response
|
||||
{
|
||||
$operations->highlight($request->user(), $id, $request->all());
|
||||
|
||||
return response()->json($operations->presentation($request->user(), $id));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Opportunity;
|
||||
use App\Services\OfferAvailability;
|
||||
use App\Services\OpportunityCanonicalContract;
|
||||
use App\Services\OpportunityManagement;
|
||||
use App\Services\OpportunityProfileWorkflow;
|
||||
use App\Services\OpportunityResources;
|
||||
use App\Services\PortalProfileAccess;
|
||||
use App\Services\PrivateOfferConfidentiality;
|
||||
use App\Services\PrivateOfferOperations;
|
||||
use App\Services\PrivateOfferPreparation;
|
||||
use App\Services\VerifiedDocumentBinary;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
|
||||
|
||||
final class PrivateOfferPreparationController extends Controller
|
||||
{
|
||||
public function resources(Request $request, PrivateOfferPreparation $service, int $id): JsonResponse
|
||||
{
|
||||
$record = $service->record($request->user(), $id);
|
||||
$resources = [];
|
||||
foreach (['images', 'documents'] as $kind) {
|
||||
$resources[$kind] = array_map(fn ($row) => ['id' => $row->id, 'version' => $row->version, 'deleted' => $row->deleted_at !== null,
|
||||
'caption' => $row->payload['caption'] ?? null, 'digitally_edited' => $row->payload['digitally_edited'] ?? null,
|
||||
'edit_description' => $row->payload['edit_description'] ?? null,
|
||||
'scan' => $row->file->malware_scan_status ?? 'pending'], app(OpportunityResources::class)->listing($id, $kind));
|
||||
}
|
||||
|
||||
return response()->json(['resources' => $resources, 'etag' => app(OpportunityManagement::class)->etag($record),
|
||||
'internalProofAccess' => app(PrivateOfferConfidentiality::class)->internal($request->user(), $record)]);
|
||||
}
|
||||
|
||||
public function attachment(Request $request, PrivateOfferPreparation $service, int $id, string $kind, string $resourceId): \Symfony\Component\HttpFoundation\Response
|
||||
{
|
||||
$record = $service->record($request->user(), $id);
|
||||
abort_unless(in_array($kind, ['documents', 'images'], true), 404);
|
||||
$link = DB::table('opportunity_'.$kind)->where('opportunity_id', $id)->where('id', $resourceId)->whereNull('deleted_at')->first();
|
||||
abort_unless($link !== null, 404);
|
||||
$document = DB::table('documents')->where('id', $link->document_id)->whereNull('deleted_at')->first();
|
||||
abort_unless($document !== null, 404);
|
||||
|
||||
return app(VerifiedDocumentBinary::class)->respond($document);
|
||||
}
|
||||
|
||||
public function confidentiality(Request $request, PrivateOfferConfidentiality $privacy, int $id): JsonResponse|Response
|
||||
{
|
||||
$overview = $privacy->overview($request->user(), $id);
|
||||
|
||||
return $request->expectsJson() ? response()->json($overview) : Inertia::render('offers/confidentiality', ['offerId' => $id]);
|
||||
}
|
||||
|
||||
public function confidentialityAction(Request $request, PrivateOfferConfidentiality $privacy, int $id): JsonResponse
|
||||
{
|
||||
return response()->json($privacy->act($request->user(), $id, $request->all()));
|
||||
}
|
||||
|
||||
public function confidentialRead(Request $request, PrivateOfferConfidentiality $privacy, int $id): JsonResponse
|
||||
{
|
||||
try {
|
||||
$result = $privacy->read($request->user(), $id);
|
||||
} catch (HttpExceptionInterface $error) {
|
||||
app(PortalProfileAccess::class)->audit($request->user(), 'offer_confidential_denied', ['record_id' => $id, 'http_status' => $error->getStatusCode()]);
|
||||
throw $error;
|
||||
}
|
||||
abort_if(isset($result['denied']), 403, 'É necessária aceitação do NDA e autorização expressa do vendedor para esta versão.');
|
||||
|
||||
return response()->json($result);
|
||||
}
|
||||
|
||||
public function nda(Request $request, PrivateOfferConfidentiality $privacy, int $id): \Symfony\Component\HttpFoundation\Response
|
||||
{
|
||||
$overview = $privacy->overview($request->user(), $id);
|
||||
abort_unless($overview['nda'], 409, 'O NDA aprovado ainda não foi disponibilizado.');
|
||||
app(PortalProfileAccess::class)->audit($request->user(), 'offer_nda_access', ['record_id' => $id, 'version_hash' => $overview['version_hash'], 'nda' => $overview['nda']]);
|
||||
|
||||
$document = DB::table('documents')->where('id', $overview['nda']['document_id'])->firstOrFail();
|
||||
abort_unless((int) $document->version_no === $overview['nda']['version'] && hash_equals(trim($document->sha256), $overview['nda']['sha256']), 409, 'O exemplar do NDA mudou; actualize antes de continuar.');
|
||||
|
||||
return app(VerifiedDocumentBinary::class)->respond($document);
|
||||
}
|
||||
|
||||
public function index(Request $request, PortalProfileAccess $access): Response
|
||||
{
|
||||
$filters = $request->validate(['state' => 'nullable|in:draft,submitted,in_review,needs_information,rejected,approved', 'operational' => 'nullable|boolean', 'pending_reports' => 'nullable|boolean']);
|
||||
$query = ($filters['operational'] ?? false) ? app(PrivateOfferOperations::class)->staffQuery($request->user()) : $access->scope(Opportunity::query(), $request->user());
|
||||
if ($filters['pending_reports'] ?? false) {
|
||||
$query = app(PrivateOfferOperations::class)->staffQuery($request->user());
|
||||
$query->whereRaw("jsonb_path_exists(COALESCE(workflow_context->'offer_reports','{}'::jsonb), '$.* ? (@.status == \"open\" || @.status == \"in_review\")')");
|
||||
}
|
||||
$records = $query->whereNotNull('workflow_context->private_offer_preparation')
|
||||
->when($filters['state'] ?? null, fn ($q, $state) => $q->where('workflow_context->private_offer_preparation->state', $state))
|
||||
->orderByDesc('id')->paginate(20)->withQueryString();
|
||||
|
||||
return Inertia::render('offers/preparation-index', ['records' => $records->through(fn ($r) => [
|
||||
'id' => $r->id, 'title' => app(OpportunityManagement::class)->values($r)['title'] ?? null, 'reference' => $r->reference,
|
||||
'state' => $access->state($r),
|
||||
'availability' => app(OfferAvailability::class)->record($r)['state'],
|
||||
]), 'filters' => $filters, 'canCreate' => count($access->navigation($request->user())['contexts']) > 0]);
|
||||
}
|
||||
|
||||
public function edit(Request $request, PrivateOfferPreparation $service, ?int $id = null): Response|JsonResponse
|
||||
{
|
||||
$record = $id ? $service->record($request->user(), $id) : null;
|
||||
$contexts = app(PortalProfileAccess::class)->navigation($request->user())['contexts'];
|
||||
abort_unless($record || count($contexts) > 0, 403, 'É necessária uma atribuição de promotor no âmbito da oferta.');
|
||||
$presentation = $record ? $service->presentation($request->user(), $record) : null;
|
||||
if ($request->expectsJson() && ! $request->header('X-Inertia')) {
|
||||
return response()->json(['record' => $presentation]);
|
||||
}
|
||||
$options = app(OpportunityCanonicalContract::class)->options($request->user(), $record);
|
||||
|
||||
return Inertia::render('offers/preparation', ['record' => $presentation, 'contexts' => $contexts,
|
||||
'contacts' => $options['contacts'], 'contactInvestors' => $options['contactInvestors'],
|
||||
'sectors' => DB::table('sectors')->where('is_active', true)->orderBy('name')->get(['id', 'name']),
|
||||
'propertyTypes' => array_values(array_filter($options['definitions']['realEstatePatch']['properties']['property_type']['enum'], fn ($type) => is_string($type)))]);
|
||||
}
|
||||
|
||||
public function save(Request $request, PrivateOfferPreparation $service, ?int $id = null): JsonResponse
|
||||
{
|
||||
if ($id) {
|
||||
$service->record($request->user(), $id, true);
|
||||
}
|
||||
$record = $service->save($request->user(), $request->all(), $id);
|
||||
|
||||
return response()->json(['record' => $service->presentation($request->user(), $record)], $id ? 200 : 201);
|
||||
}
|
||||
|
||||
public function submit(Request $request, PrivateOfferPreparation $service, int $id): JsonResponse
|
||||
{
|
||||
$record = $service->submit($request->user(), $id, $request->all());
|
||||
|
||||
return response()->json(['record' => $service->presentation($request->user(), $record),
|
||||
'message' => 'Submissão privada registada. A oferta não foi publicada.']);
|
||||
}
|
||||
|
||||
public function transition(Request $request, PrivateOfferPreparation $service, OpportunityProfileWorkflow $workflow, int $id): JsonResponse
|
||||
{
|
||||
$service->record($request->user(), $id);
|
||||
$data = $request->validate(['etag' => 'required|string|size:64', 'action' => 'required|in:start_review,assign_review,request_correction,reject,approve,request_revision',
|
||||
'assignee_user_id' => 'required_if:action,assign_review|integer|min:1',
|
||||
'reason' => 'required|string|min:20|max:1000', 'correction_items' => 'sometimes|array', 'correction_items.*' => 'string']);
|
||||
$record = $workflow->transition($request->user(), $id, $data);
|
||||
|
||||
return response()->json(['record' => $service->presentation($request->user(), $record), 'message' => 'Decisão privada registada. A oferta não foi publicada.']);
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,14 @@ class ProtectProfileOpportunity
|
||||
}
|
||||
if ($request->user() && $id !== null && Schema::hasColumn('opportunities', 'owner_user_id')) {
|
||||
$record = Opportunity::withTrashed()->find($id);
|
||||
if ($record?->isPrivateOfferPreparation()) {
|
||||
app(PortalProfileAccess::class)->authorize($request->user(), $request->isMethod('GET') ? 'view' : 'edit', $record);
|
||||
if ($request->isMethod('GET') && $request->is('minhas-oportunidades/*/edit', 'minhas-oportunidades/*/view', 'management/opportunities/*/edit', 'management/opportunities/*/view')) {
|
||||
return redirect('/minhas-oportunidades/ofertas/'.$record->id)->header('Cache-Control', 'private, no-store');
|
||||
}
|
||||
// Contact registration reuses the CRM master and its fresh-MFA gate.
|
||||
abort_unless($request->isMethod('POST') && $request->is('minhas-oportunidades/*/contacts'), 403, 'Use a preparação privada da oferta.');
|
||||
}
|
||||
if ($record && $record->owner_user_id === null && ! $request->isMethod('GET')) {
|
||||
abort_unless(app(PortalProfileAccess::class)->legacyWriteAllowed($request->user()), 403, 'O papel v1 não contorna os controlos através de um registo legado.');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\Opportunity;
|
||||
use App\Models\User;
|
||||
use App\Services\PortalEditorialWorkflow;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Queue\Queueable;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
|
||||
/** Existing email transport and operational allowlist; no confidential payload. */
|
||||
final class NotifyPrivateOfferDecision implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public int $tries = 3;
|
||||
|
||||
public int $timeout = 30;
|
||||
|
||||
public function __construct(public string $noticeId) {}
|
||||
|
||||
/** @return list<int> */
|
||||
public function backoff(): array
|
||||
{
|
||||
return [60, 300, 900];
|
||||
}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
if (! config('portal.notifications_enabled')) {
|
||||
return;
|
||||
}
|
||||
$notice = DB::table('portal_editorial_notifications')->where('id', $this->noticeId)->first();
|
||||
$user = $notice ? User::query()->whereKey($notice->user_id)->first() : null;
|
||||
$record = $notice ? Opportunity::query()->whereKey($notice->opportunity_id)->first() : null;
|
||||
if (! $notice || ! $user || ! $record?->isPrivateOfferPreparation()
|
||||
|| ! in_array($user->email, config('portal.notification_recipients', []), true)
|
||||
|| ! app(PortalEditorialWorkflow::class)->visibleNotice($user, $notice)) {
|
||||
return;
|
||||
}
|
||||
$body = 'Existe uma actualização na revisão privada da oferta '.$record->reference.'. Consulte a decisão e a versão na área protegida: '.url('/minhas-oportunidades/ofertas/'.$record->id);
|
||||
Mail::raw($body, fn ($message) => $message->to($user->email)->subject('Actualização da oferta — '.$record->reference));
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,23 @@ class Opportunity extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
public function isPrivateOfferPreparation(): bool
|
||||
{
|
||||
return isset($this->workflow_context['private_offer_preparation']);
|
||||
}
|
||||
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::saving(function (self $record) {
|
||||
$original = $record->getRawOriginal('workflow_context');
|
||||
$wasPrivate = is_string($original) && isset(json_decode($original, true)['private_offer_preparation']);
|
||||
abort_if($wasPrivate && ! $record->isPrivateOfferPreparation(), 403, 'A preparação privada não pode ser convertida neste percurso.');
|
||||
if ($record->isPrivateOfferPreparation()) {
|
||||
abort_unless($record->status === 'draft' && $record->published_at === null, 403, 'A publicação de Ofertas não está disponível nesta fase.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected $hidden = ['management_draft', 'management_creation_key', 'management_version', 'first_submitted_at', 'management_closed_once', 'owner_user_id', 'organization_id', 'workflow_context'];
|
||||
|
||||
protected $appends = ['maturity_catalogue_version'];
|
||||
@@ -115,6 +132,7 @@ class Opportunity extends Model
|
||||
public function scopePublished(Builder $query): Builder
|
||||
{
|
||||
if (Schema::hasColumn('opportunities', 'workflow_context')) {
|
||||
$query->whereNull('workflow_context->private_offer_preparation');
|
||||
$query->where(fn ($q) => $q->whereNull('owner_user_id')->orWhere('workflow_context->publication->visibility', 'public'));
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ class InvestmentCatalogue
|
||||
'projects' => ['project_id', 'projects'], 'locations' => ['geo_location_id', 'geo_locations']];
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
public function filters(Request $request): array
|
||||
public function filters(Request $request, bool $offerPrices = false): array
|
||||
{
|
||||
$filters = $request->validate(['perspective' => ['nullable', 'in:exists,needs,ideas,opportunities'], 'q' => ['nullable', 'string', 'max:120'], 'sector' => ['nullable', 'string', 'max:80'],
|
||||
'preparation' => ['nullable', Rule::in(array_column(OpportunityMaturity::searchOptions(), 'value'))],
|
||||
@@ -34,9 +34,9 @@ class InvestmentCatalogue
|
||||
'zee_id' => ['nullable', 'uuid'], 'zfi_id' => ['nullable', 'uuid'], 'park_id' => ['nullable', 'uuid'],
|
||||
'dimension' => ['nullable', 'in:cluster,corridor,zee,zfi'], 'view' => ['nullable', 'in:grid,list,map,combined,corridors'],
|
||||
'maturity_level' => ['nullable', 'integer', 'between:1,8'], 'facets' => ['nullable', 'array', 'max:20'], 'facets.*' => ['integer', 'min:1'],
|
||||
'min_amount' => ['nullable', 'numeric', 'min:0', 'max:999999999999'],
|
||||
'max_amount' => ['nullable', 'numeric', 'min:0', 'max:999999999999', ...($request->filled('min_amount') ? ['gte:min_amount'] : [])],
|
||||
'currency' => ['nullable', 'required_with:min_amount,max_amount', 'in:MZN,USD,EUR,ZAR'],
|
||||
'min_amount' => $offerPrices ? ['nullable', 'string', 'regex:/^\d{1,16}(\.\d{1,2})?$/'] : ['nullable', 'numeric', 'min:0', 'max:999999999999'],
|
||||
'max_amount' => $offerPrices ? ['nullable', 'string', 'regex:/^\d{1,16}(\.\d{1,2})?$/'] : ['nullable', 'numeric', 'min:0', 'max:999999999999', ...($request->filled('min_amount') ? ['gte:min_amount'] : [])],
|
||||
'currency' => ['nullable', 'required_with:min_amount,max_amount', $offerPrices ? 'in:MZN,USD' : 'in:MZN,USD,EUR,ZAR'],
|
||||
'before' => ['nullable', 'date_format:Y-m-d'], 'sort' => ['nullable', 'in:recent,title,amount_asc,amount_desc'],
|
||||
'association_type' => ['nullable', 'in:'.implode(',', array_keys(OpportunityClassification::TARGETS))],
|
||||
'association_target' => ['nullable', 'string', 'max:80'],
|
||||
|
||||
@@ -193,6 +193,17 @@ final class InvestorContext
|
||||
$href = $published ? '/oportunidades/'.$o->slug : '/minhas-oportunidades/'.$o->id.'/view';
|
||||
$version = $o->management_version;
|
||||
$fields = ['project_title' => $title, 'project_summary' => $draft['canonical']['executive_summary'] ?? $o->summary];
|
||||
if ($o->isPrivateOfferPreparation()) {
|
||||
// Offers reuse Opportunity, but their preparation writes description,
|
||||
// not the canonical executive summary. Keep this authorised private
|
||||
// projection explicit: never copy private_offer (price/finances/contact).
|
||||
$title = $draft['title'] ?? $o->title;
|
||||
$summary = $fields['project_summary'];
|
||||
$fields['project_title'] = $title;
|
||||
$fields['project_summary'] = is_string($summary) && trim($summary) !== ''
|
||||
? $summary : ($draft['description'] ?? null);
|
||||
$href = '/minhas-oportunidades/ofertas/'.$o->id;
|
||||
}
|
||||
if ($o->sector) {
|
||||
$fields['sector_id'] = (string) $o->sector_id;
|
||||
$fields['sector'] = $o->sector->name;
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Opportunity;
|
||||
use Carbon\CarbonImmutable;
|
||||
|
||||
/** Read-time exclusion never depends on the scheduler or the NDA. */
|
||||
final class OfferAvailability
|
||||
{
|
||||
/** @return array{state:string,expires_at:?string,policy_pending:bool} */
|
||||
public function view(mixed $operation, mixed $publication): array
|
||||
{
|
||||
// Legacy public snapshots without an editorial hash retain their first frozen policy.
|
||||
$key = is_array($publication) ? ($publication['editorial_hash'] ?? '') : null;
|
||||
$key ??= is_array($operation) ? ($operation['last_approval_hash'] ?? '') : '';
|
||||
if (! is_string($key)) {
|
||||
return ['state' => 'unavailable', 'expires_at' => null, 'policy_pending' => true];
|
||||
}
|
||||
$policy = is_array($operation) ? ($operation['validity_policies'][$key] ?? $operation['validity'] ?? null) : null;
|
||||
$pending = ! is_array($policy) || ($policy['days'] ?? null) === null;
|
||||
$result = ['state' => 'available', 'expires_at' => null, 'policy_pending' => $pending];
|
||||
if ($operation !== null && ! is_array($operation)) {
|
||||
return [...$result, 'state' => 'unavailable'];
|
||||
}
|
||||
$state = $operation['status'] ?? 'available';
|
||||
if (! is_int($operation['cycle'] ?? 0) || ($operation['cycle'] ?? 0) < 0) {
|
||||
return [...$result, 'state' => 'unavailable'];
|
||||
}
|
||||
if (! in_array($state, ['available', 'withdrawn', 'expired', 'revalidation'], true)) {
|
||||
return [...$result, 'state' => 'unavailable'];
|
||||
}
|
||||
if ($state !== 'available') {
|
||||
return [...$result, 'state' => $state];
|
||||
}
|
||||
$days = $policy['days'] ?? null;
|
||||
if (($operation['cycle'] ?? 0) > 0 && ($publication['offer_cycle'] ?? null) !== $operation['cycle']) {
|
||||
return $result;
|
||||
}
|
||||
if ($days === null || ! is_array($publication) || ($publication['publication']['state'] ?? null) !== 'published') {
|
||||
return $result;
|
||||
}
|
||||
if (! is_int($days) || $days < 1 || $days > 36500) {
|
||||
return [...$result, 'state' => 'unavailable'];
|
||||
}
|
||||
try {
|
||||
$at = $publication['publication']['at'] ?? null;
|
||||
if (! is_string($at) || ! preg_match('/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?(?:Z|[+-]\d{2}:\d{2})$/D', $at)) {
|
||||
return [...$result, 'state' => 'unavailable'];
|
||||
}
|
||||
$start = CarbonImmutable::parse($at);
|
||||
$end = $start->addDays($days)->utc();
|
||||
} catch (\Throwable) {
|
||||
return [...$result, 'state' => 'unavailable'];
|
||||
}
|
||||
|
||||
return [...$result, 'expires_at' => $end->toIso8601String(), 'state' => $end->lte(CarbonImmutable::now()) ? 'expired' : 'available'];
|
||||
}
|
||||
|
||||
/** @return array{state:string,expires_at:?string,policy_pending:bool} */
|
||||
public function record(Opportunity $record): array
|
||||
{
|
||||
return $this->view($record->workflow_context['offer_operation'] ?? null, $record->workflow_context['offer_publication'] ?? null);
|
||||
}
|
||||
|
||||
public function publicAllowed(mixed $operation, mixed $publication): bool
|
||||
{
|
||||
if ($this->view($operation, $publication)['state'] !== 'available') {
|
||||
return false;
|
||||
}
|
||||
$cycle = $operation['cycle'] ?? 0;
|
||||
if (! is_int($cycle) || $cycle < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// A new validation must never resurrect the previous public snapshot.
|
||||
return $cycle === 0 || (is_array($publication) && ($publication['offer_cycle'] ?? null) === $cycle
|
||||
&& is_string($operation['activated_hash'] ?? null) && ($publication['editorial_hash'] ?? null) === $operation['activated_hash']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Opportunity;
|
||||
use App\Support\IsolatedQaDatabase;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
/** Read-only consumer. Effective publication remains unavailable until confidentiality is delivered.
|
||||
* The injected fixture mode is never configurable by HTTP, environment flags or stored content. */
|
||||
final class OfferPublicCatalogue
|
||||
{
|
||||
public const CONTRACT = 'offer-public-projection-v1';
|
||||
|
||||
public function __construct(private bool $syntheticFixtureMode = false) {}
|
||||
|
||||
private function fixtureReadAllowed(): bool
|
||||
{
|
||||
return $this->syntheticFixtureMode && IsolatedQaDatabase::allowsSchemaChanges();
|
||||
}
|
||||
|
||||
/** Exact ordering of decimal strings, without currency conversion or floating point.
|
||||
* @param string $a @param string $b */
|
||||
public static function comparePrice(string $a, string $b): int
|
||||
{
|
||||
$parts = static function (string $value): array {
|
||||
[$whole, $fraction] = [...explode('.', $value), ''];
|
||||
|
||||
return [ltrim($whole, '0') ?: '0', str_pad($fraction, 2, '0')];
|
||||
};
|
||||
[$aw, $af] = $parts($a);
|
||||
[$bw, $bf] = $parts($b);
|
||||
|
||||
return (strlen($aw) <=> strlen($bw)) ?: (strcmp($aw, $bw) <=> 0) ?: (strcmp($af, $bf) <=> 0);
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
public function filters(Request $request): array
|
||||
{
|
||||
$filters = array_intersect_key(app(InvestmentCatalogue::class)->filters($request, true), array_flip([
|
||||
'q', 'category', 'province_id', 'district_id', 'currency', 'min_amount', 'max_amount', 'sort', 'view', 'page', 'per_page',
|
||||
]));
|
||||
$extra = $request->validate(['price_mode' => 'nullable|in:amount,negotiable']);
|
||||
$filters = [...$filters, ...$extra];
|
||||
if (isset($filters['min_amount'], $filters['max_amount']) && self::comparePrice($filters['min_amount'], $filters['max_amount']) > 0) {
|
||||
throw ValidationException::withMessages(['max_amount' => 'O preço máximo deve ser igual ou superior ao mínimo.']);
|
||||
}
|
||||
if (in_array($filters['sort'] ?? '', ['amount_asc', 'amount_desc'], true) && empty($filters['currency'])) {
|
||||
throw ValidationException::withMessages(['currency' => 'Seleccione uma moeda para ordenar por preço.']);
|
||||
}
|
||||
if (($filters['price_mode'] ?? '') === 'negotiable' && (isset($filters['min_amount']) || isset($filters['max_amount']))) {
|
||||
throw ValidationException::withMessages(['price_mode' => 'Retire o intervalo de montantes para procurar preços a negociar.']);
|
||||
}
|
||||
if (($filters['price_mode'] ?? '') === 'negotiable' && ! empty($filters['currency'])) {
|
||||
throw ValidationException::withMessages(['currency' => 'A moeda de um preço a negociar não foi indicada. Retire este filtro.']);
|
||||
}
|
||||
|
||||
return $filters;
|
||||
}
|
||||
|
||||
/** The explicit public snapshot, never a projection of current private columns.
|
||||
* @return array<string,mixed>|null */
|
||||
private function project(int $id, mixed $publication): ?array
|
||||
{
|
||||
if (! is_array($publication) || ($publication['contract'] ?? null) !== self::CONTRACT || ($publication['qa_fixture'] ?? false) !== true) {
|
||||
return null;
|
||||
}
|
||||
$v = Validator::make($publication, [
|
||||
'payload' => 'required|array:title,summary,category,province_id,district_id,price_mode,amount,currency',
|
||||
'payload.title' => 'required|string|max:150', 'payload.summary' => 'required|string|max:1500',
|
||||
'payload.category' => ['required', Rule::in(OpportunityManagement::CATEGORIES)],
|
||||
'payload.province_id' => 'nullable|uuid', 'payload.district_id' => 'nullable|uuid',
|
||||
'payload.price_mode' => 'required|in:amount,negotiable',
|
||||
'payload.amount' => ['nullable', 'string', 'required_if:payload.price_mode,amount', 'prohibited_unless:payload.price_mode,amount', 'regex:/^\d{1,16}(\.\d{1,2})?$/'],
|
||||
'payload.currency' => 'nullable|required_if:payload.price_mode,amount|prohibited_unless:payload.price_mode,amount|in:MZN,USD',
|
||||
'approval.state' => 'required|in:approved', 'approval.hash' => 'required|string|size:64', 'approval.actor_id' => 'required|integer|min:1',
|
||||
'approval.at' => 'required|date', 'publication.state' => 'required|in:published', 'publication.hash' => 'required|string|size:64',
|
||||
'publication.actor_id' => 'required|integer|min:1|different:approval.actor_id', 'publication.at' => 'required|date|after_or_equal:approval.at|before_or_equal:now',
|
||||
'revision' => 'required|integer|min:1',
|
||||
]);
|
||||
if ($v->fails()) {
|
||||
return null;
|
||||
}
|
||||
$p = $v->validated()['payload'];
|
||||
$hash = self::projectionHash($p);
|
||||
if (! hash_equals($hash, $publication['approval']['hash']) || ! hash_equals($hash, $publication['publication']['hash'])) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
$path = app(InvestmentGeography::class)->selection(['province_id' => $p['province_id'] ?? null, 'district_id' => $p['district_id'] ?? null]);
|
||||
} catch (ValidationException $error) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ['id' => $id, 'title' => $p['title'], 'summary' => $p['summary'], 'category' => $p['category'],
|
||||
'location' => array_map(fn ($node) => ['id' => $node->id, 'name' => $node->name, 'level' => $node->location_type], $path),
|
||||
'price' => ['mode' => $p['price_mode'], 'amount' => $p['amount'] ?? null, 'currency' => $p['currency'] ?? null],
|
||||
'status' => 'published', 'published_at' => CarbonImmutable::parse($publication['publication']['at'])->utc()->toIso8601String(), 'revision' => $publication['revision'], 'url' => '/ofertas/'.$id];
|
||||
}
|
||||
|
||||
/** @param array<string,mixed> $payload */
|
||||
public static function projectionHash(array $payload): string
|
||||
{
|
||||
ksort($payload);
|
||||
|
||||
return hash('sha256', json_encode(['contract' => self::CONTRACT, 'payload' => $payload], JSON_THROW_ON_ERROR));
|
||||
}
|
||||
|
||||
/** @param array<string,mixed> $filters
|
||||
* @return array{records:list<array<string,mixed>>,truncated:bool} */
|
||||
public function records(array $filters = []): array
|
||||
{
|
||||
if (! $this->fixtureReadAllowed()) {
|
||||
return ['records' => [], 'truncated' => false];
|
||||
}
|
||||
$rows = Opportunity::query()->whereNotNull('workflow_context->private_offer_preparation')->whereNotNull('workflow_context->offer_publication')
|
||||
->orderBy('id')->limit(1001)->get(['id', DB::raw("workflow_context->'offer_publication' AS public_snapshot"), DB::raw("workflow_context->'offer_operation' AS operation_state")]);
|
||||
$records = [];
|
||||
foreach ($rows->take(1000) as $row) {
|
||||
if (! is_string($row->getAttribute('public_snapshot'))) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
$publication = json_decode($row->getAttribute('public_snapshot'), true, 512, JSON_THROW_ON_ERROR);
|
||||
$operation = json_decode($row->getAttribute('operation_state') ?? 'null', true, 512, JSON_THROW_ON_ERROR);
|
||||
} catch (\JsonException $error) {
|
||||
continue;
|
||||
}
|
||||
if (! app(OfferAvailability::class)->publicAllowed($operation, $publication)) {
|
||||
continue;
|
||||
}
|
||||
$p = $this->project($row->id, $publication);
|
||||
if (! $p) {
|
||||
continue;
|
||||
}
|
||||
$expires = app(OfferAvailability::class)->view($operation, $publication)['expires_at'];
|
||||
if ($expires !== null) {
|
||||
$p['expires_at'] = $expires;
|
||||
}
|
||||
if (! empty($filters['q']) && ! str_contains(mb_strtolower($p['title'].' '.$p['summary']), mb_strtolower($filters['q']))) {
|
||||
continue;
|
||||
}
|
||||
if (! empty($filters['category']) && $p['category'] !== $filters['category']) {
|
||||
continue;
|
||||
}
|
||||
if (! empty($filters['province_id']) && ($p['location'][0]['id'] ?? null) !== $filters['province_id']) {
|
||||
continue;
|
||||
}
|
||||
if (! empty($filters['district_id']) && ($p['location'][1]['id'] ?? null) !== $filters['district_id']) {
|
||||
continue;
|
||||
}
|
||||
if (! empty($filters['price_mode']) && $p['price']['mode'] !== $filters['price_mode']) {
|
||||
continue;
|
||||
}
|
||||
if (! empty($filters['currency']) && $p['price']['currency'] !== $filters['currency']) {
|
||||
continue;
|
||||
}
|
||||
if (isset($filters['min_amount']) && ($p['price']['amount'] === null || self::comparePrice($p['price']['amount'], $filters['min_amount']) < 0)) {
|
||||
continue;
|
||||
}
|
||||
if (isset($filters['max_amount']) && ($p['price']['amount'] === null || self::comparePrice($p['price']['amount'], $filters['max_amount']) > 0)) {
|
||||
continue;
|
||||
}
|
||||
$records[] = $p;
|
||||
}
|
||||
$sort = $filters['sort'] ?? 'recent';
|
||||
usort($records, static function ($a, $b) use ($sort): int {
|
||||
if ($sort === 'title') {
|
||||
return strnatcasecmp($a['title'], $b['title']) ?: ($a['id'] <=> $b['id']);
|
||||
}
|
||||
if (in_array($sort, ['amount_asc', 'amount_desc'], true)) {
|
||||
if ($a['price']['amount'] === null || $b['price']['amount'] === null) {
|
||||
return ($a['price']['amount'] === null) <=> ($b['price']['amount'] === null);
|
||||
}
|
||||
|
||||
return self::comparePrice($a['price']['amount'], $b['price']['amount']) * ($sort === 'amount_desc' ? -1 : 1) ?: ($a['id'] <=> $b['id']);
|
||||
}
|
||||
|
||||
return strcmp($b['published_at'], $a['published_at']) ?: ($b['id'] <=> $a['id']);
|
||||
});
|
||||
|
||||
return ['records' => $records, 'truncated' => $rows->count() > 1000];
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
public function detail(int $id): array
|
||||
{
|
||||
abort_unless($this->fixtureReadAllowed(), 404);
|
||||
$row = Opportunity::query()->whereKey($id)->whereNotNull('workflow_context->private_offer_preparation')->first(['id', DB::raw("workflow_context->'offer_publication' AS public_snapshot"), DB::raw("workflow_context->'offer_operation' AS operation_state")]);
|
||||
if (! $row || ! is_string($row->getAttribute('public_snapshot'))) {
|
||||
abort(404);
|
||||
}
|
||||
try {
|
||||
$publication = json_decode($row->getAttribute('public_snapshot'), true, 512, JSON_THROW_ON_ERROR);
|
||||
$operation = json_decode($row->getAttribute('operation_state') ?? 'null', true, 512, JSON_THROW_ON_ERROR);
|
||||
} catch (\JsonException $error) {
|
||||
abort(404);
|
||||
}
|
||||
abort_unless(app(OfferAvailability::class)->publicAllowed($operation, $publication), 404);
|
||||
$record = $this->project($id, $publication);
|
||||
abort_unless($record !== null, 404);
|
||||
$expires = app(OfferAvailability::class)->view($operation, $publication)['expires_at'];
|
||||
if ($expires !== null) {
|
||||
$record['expires_at'] = $expires;
|
||||
}
|
||||
|
||||
return $record;
|
||||
}
|
||||
|
||||
/** @param array<string,mixed> $filters
|
||||
* @return array{results:LengthAwarePaginator<int,array<string,mixed>>,truncated:bool} */
|
||||
public function page(Request $request, array $filters): array
|
||||
{
|
||||
$result = $this->records($filters);
|
||||
$size = (int) ($filters['per_page'] ?? 12);
|
||||
$page = (int) ($filters['page'] ?? 1);
|
||||
|
||||
return ['results' => new LengthAwarePaginator(array_slice($result['records'], ($page - 1) * $size, $size), count($result['records']), $size, $page,
|
||||
['path' => $request->url(), 'query' => $filters]), 'truncated' => $result['truncated']];
|
||||
}
|
||||
|
||||
/** @param array<string,mixed> $filters
|
||||
* @return array<string,mixed> */
|
||||
public function map(array $filters): array
|
||||
{
|
||||
$result = $this->records($filters);
|
||||
$areas = [];
|
||||
$seen = [];
|
||||
foreach ($result['records'] as $record) {
|
||||
$id = $record['location'][array_key_last($record['location'])]['id'] ?? null;
|
||||
if (! $id || isset($seen[$id])) {
|
||||
continue;
|
||||
}
|
||||
$seen[$id] = true;
|
||||
$areas = [...$areas, ...app(TerritoryGeometry::class)->context($id)['areas']];
|
||||
if (count($seen) >= 30) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return ['features' => [], 'areas' => $areas, 'matched_count' => count($result['records']), 'exact_locations_disclosed' => false,
|
||||
'truncated' => $result['truncated'] || count($seen) >= 30];
|
||||
}
|
||||
}
|
||||
@@ -279,14 +279,7 @@ class OpportunityCanonicalContract
|
||||
'economic_activity_id', 'Sector, subsector e atividade devem corresponder à hierarquia/versão publicada; os cinco níveis originais são conservados.');
|
||||
foreach (['internal_contact_id', 'public_contact_id'] as $field) {
|
||||
if (! empty($payload[$field])) {
|
||||
$contacts = DB::table('contacts')->where('contacts.id', $payload[$field])->whereNull('contacts.deleted_at');
|
||||
if ($record->organization_id) {
|
||||
$contacts->whereExists(fn ($q) => $q->selectRaw('1')->from('investor_contacts as ic')->join('investors as i', 'i.id', '=', 'ic.investor_id')
|
||||
->whereColumn('ic.contact_id', 'contacts.id')->where('i.organization_id', $record->organization_id)->whereNull('i.deleted_at'));
|
||||
} else {
|
||||
$contacts->where('source_system', 'portal-user-contact')->where('external_id', 'user:'.$record->owner_user_id);
|
||||
}
|
||||
$check($contacts->exists(), $field, 'Contacto inexistente ou sem relação com o Promotor.');
|
||||
$check(app(OpportunityContacts::class)->related($record, $payload[$field]), $field, 'Contacto inexistente ou sem relação com o Promotor.');
|
||||
}
|
||||
}
|
||||
$resources = [];
|
||||
|
||||
@@ -14,6 +14,19 @@ use Illuminate\Validation\ValidationException;
|
||||
/** Reuse the CRM master without manufacturing an interest, lead or consent. */
|
||||
class OpportunityContacts
|
||||
{
|
||||
public function related(Opportunity $record, string $id): bool
|
||||
{
|
||||
$contacts = DB::table('contacts')->where('contacts.id', $id)->whereNull('contacts.deleted_at');
|
||||
if ($record->organization_id) {
|
||||
$contacts->whereExists(fn ($q) => $q->selectRaw('1')->from('investor_contacts as ic')->join('investors as i', 'i.id', '=', 'ic.investor_id')
|
||||
->whereColumn('ic.contact_id', 'contacts.id')->where('i.organization_id', $record->organization_id)->whereNull('i.deleted_at'));
|
||||
} else {
|
||||
$contacts->where('source_system', 'portal-user-contact')->where('external_id', 'user:'.$record->owner_user_id);
|
||||
}
|
||||
|
||||
return $contacts->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<array-key, mixed> $input
|
||||
* @return array<array-key, mixed>
|
||||
|
||||
@@ -45,6 +45,7 @@ class OpportunityLibrarySupport
|
||||
*/
|
||||
public function attach(User $actor, Opportunity $opportunity, array $input): \stdClass
|
||||
{
|
||||
abort_if($opportunity->isPrivateOfferPreparation(), 422, 'Os comprovativos de Ofertas usam originais reservados à validação interna.');
|
||||
app(PortalProfileAccess::class)->authorize($actor, 'edit', $opportunity);
|
||||
$data = Validator::make($input, ['resource_id' => 'required|integer|min:1', 'creation_key' => 'required|uuid', 'etag' => 'required|string|size:64', 'reason' => 'required|string|min:20|max:1000', 'section' => 'nullable|string|max:255', 'supported_statement' => 'nullable|string|max:2000'])->validate();
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ class OpportunityManagement
|
||||
|
||||
public const MATURITY_FIELDS = ['maturity_level', 'maturity_label', 'maturity_stage', 'maturity_catalogue_version'];
|
||||
|
||||
public const PRIVATE_DRAFT_FIELDS = [...self::MATURITY_FIELDS, 'real_estate', 'canonical', 'proactive'];
|
||||
public const PRIVATE_DRAFT_FIELDS = [...self::MATURITY_FIELDS, 'real_estate', 'canonical', 'proactive', 'private_offer'];
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
public function values(Opportunity $record): array
|
||||
@@ -51,12 +51,13 @@ class OpportunityManagement
|
||||
}
|
||||
|
||||
/** @param array<string,mixed> $input */
|
||||
public function save(User $actor, array $input, ?int $id = null): Opportunity
|
||||
public function save(User $actor, array $input, ?int $id = null, bool $privateOffer = false): Opportunity
|
||||
{
|
||||
$access = app(PortalProfileAccess::class);
|
||||
$profileCreation = $id === null && str_starts_with(request()->path(), 'minhas-oportunidades');
|
||||
if ($id !== null) {
|
||||
$existingRecord = Opportunity::withTrashed()->findOrFail($id);
|
||||
abort_if($existingRecord->isPrivateOfferPreparation() && ! $privateOffer, 403, 'Use a preparação privada da oferta.');
|
||||
$access->authorize($actor, 'edit', $existingRecord);
|
||||
if ($existingRecord->owner_user_id !== null) {
|
||||
abort_unless($access->canEdit($actor, $existingRecord, $input['opportunity_category'] ?? null), 403);
|
||||
@@ -100,6 +101,9 @@ class OpportunityManagement
|
||||
'owner_user_id' => 'prohibited', 'workflow_context' => 'prohibited', 'status' => 'prohibited', 'first_submitted_at' => 'prohibited',
|
||||
'province_id' => ['nullable', 'uuid'], 'district_id' => ['nullable', 'uuid'], 'admin_post_id' => ['nullable', 'uuid'], 'locality_id' => ['nullable', 'uuid'],
|
||||
])->validate();
|
||||
if ($privateOffer && ($id === null || array_key_exists('private_offer', $input))) {
|
||||
$data['private_offer'] = app(PrivateOfferPreparation::class)->validate($input['private_offer'] ?? []);
|
||||
}
|
||||
$data = app(OpportunityMaturity::class)->normalize($data);
|
||||
if (array_key_exists('real_estate', $data)) {
|
||||
$data['real_estate'] = app(OpportunityRealEstateDraft::class)->validate($data['real_estate']);
|
||||
@@ -108,10 +112,11 @@ class OpportunityManagement
|
||||
$data['canonical'] = app(OpportunityCanonicalContract::class)->draft($data['canonical']);
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($actor, $data, $id, $profileCreation, $access) {
|
||||
return DB::transaction(function () use ($actor, $data, $id, $profileCreation, $access, $privateOffer) {
|
||||
DB::select('SELECT pg_advisory_xact_lock(91026, 100000)');
|
||||
if (! $id && ($existing = Opportunity::withTrashed()->where('management_creation_key', $data['creation_key'])->first())) {
|
||||
$access->authorize($actor, 'view', $existing);
|
||||
abort_unless($existing->isPrivateOfferPreparation() === $privateOffer, 409);
|
||||
abort_unless($profileCreation === ($existing->owner_user_id !== null) && (! $profileCreation || $existing->organization_id === ($data['organization_id'] ?? null)), 409);
|
||||
$snapshot = $this->values($existing);
|
||||
foreach (array_intersect_key($data, array_flip([...self::FIELDS, ...self::PRIVATE_DRAFT_FIELDS])) as $key => $value) {
|
||||
@@ -124,6 +129,7 @@ class OpportunityManagement
|
||||
}
|
||||
$record = $id ? Opportunity::withTrashed()->whereKey($id)->lockForUpdate()->firstOrFail() : new Opportunity;
|
||||
if ($id) {
|
||||
abort_unless($record->isPrivateOfferPreparation() === $privateOffer, 409);
|
||||
$access->authorize($actor, 'edit', $record);
|
||||
$this->check($record, $data['etag']);
|
||||
abort_if($record->trashed() || $record->status === 'archived', 409, 'Restaure antes de editar.');
|
||||
@@ -189,14 +195,20 @@ class OpportunityManagement
|
||||
$record->management_closed_once = true;
|
||||
$previous = $record->management_draft ? json_decode($record->management_draft, true) : $record->only(self::FIELDS);
|
||||
$record->management_draft = json_encode([...$previous, ...$changes], JSON_THROW_ON_ERROR);
|
||||
if ($privateOffer) {
|
||||
// All columns of an Offer remain private; keep scope/category and location current.
|
||||
foreach (array_intersect_key($changes, array_flip(self::FIELDS)) as $field => $value) {
|
||||
$record->{$field} = $value;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
foreach ($changes as $key => $value) {
|
||||
if (in_array($key, ['real_estate', 'canonical', 'proactive', 'maturity_stage', 'maturity_catalogue_version'], true) || (($changes['maturity_catalogue_version'] ?? null) === OpportunityMaturity::VERSION && in_array($key, ['maturity_level', 'maturity_label'], true))) {
|
||||
if (in_array($key, ['real_estate', 'canonical', 'proactive', 'private_offer', 'maturity_stage', 'maturity_catalogue_version'], true) || (($changes['maturity_catalogue_version'] ?? null) === OpportunityMaturity::VERSION && in_array($key, ['maturity_level', 'maturity_label'], true))) {
|
||||
continue;
|
||||
}
|
||||
$record->{$key} = $value;
|
||||
}
|
||||
if (isset($changes['maturity_catalogue_version']) || array_key_exists('real_estate', $changes) || array_key_exists('canonical', $changes) || array_key_exists('proactive', $changes)) {
|
||||
if (isset($changes['maturity_catalogue_version']) || array_key_exists('real_estate', $changes) || array_key_exists('canonical', $changes) || array_key_exists('proactive', $changes) || array_key_exists('private_offer', $changes)) {
|
||||
// The existing draft JSON holds the versioned catalogue; legacy columns stay intact.
|
||||
$previous = $record->management_draft ? json_decode($record->management_draft, true, 512, JSON_THROW_ON_ERROR) : [];
|
||||
$record->management_draft = json_encode([...$previous, ...array_intersect_key($changes, array_flip(self::PRIVATE_DRAFT_FIELDS))], JSON_THROW_ON_ERROR);
|
||||
@@ -204,6 +216,9 @@ class OpportunityManagement
|
||||
}
|
||||
if ($record->owner_user_id !== null) {
|
||||
$context = $record->workflow_context ?? [];
|
||||
if ($privateOffer) {
|
||||
$context['private_offer_preparation'] ??= ['version' => 1, 'state' => 'draft'];
|
||||
}
|
||||
unset($context['approval']);
|
||||
$context['content_revision'] = ($context['content_revision'] ?? 0) + 1;
|
||||
$context['contributors'] = array_values(array_unique([...($context['contributors'] ?? []), $actor->id]));
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Services;
|
||||
|
||||
use App\Models\Opportunity;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Str;
|
||||
@@ -27,6 +28,9 @@ class OpportunityProfileWorkflow
|
||||
if ($record->owner_user_id === null || ! $this->access->canView($actor, $record)) {
|
||||
return [];
|
||||
}
|
||||
if ($record->isPrivateOfferPreparation()) {
|
||||
return $this->offerActions($actor, $record);
|
||||
}
|
||||
$actions = [];
|
||||
$state = $this->access->state($record);
|
||||
if ($this->access->promoterRecordScope($actor, $record)
|
||||
@@ -99,6 +103,44 @@ class OpportunityProfileWorkflow
|
||||
return $actions;
|
||||
}
|
||||
|
||||
/** Existing roles and transition engine, bounded to the private offer contract.
|
||||
* @return list<string> */
|
||||
private function offerActions(User $actor, Opportunity $record): array
|
||||
{
|
||||
if (! app(PortalEditorialWorkflow::class)->available()) {
|
||||
return [];
|
||||
}
|
||||
$state = $this->access->state($record);
|
||||
$actions = [];
|
||||
if ($this->access->promoterRecordScope($actor, $record) && $this->access->operational($actor, 'edit', $record->id)) {
|
||||
if (in_array($state, ['draft', 'needs_information'], true)) {
|
||||
$actions[] = 'submit';
|
||||
}
|
||||
if (in_array($state, ['rejected', 'approved'], true) || (isset($record->workflow_context['submission']) && ($record->workflow_context['offer_operation']['status'] ?? null) === 'revalidation'
|
||||
&& ($record->workflow_context['offer_operation']['requested_by'] ?? null) === $actor->id && (int) $record->owner_user_id === $actor->id
|
||||
&& in_array($state, ['draft', 'needs_information', 'submitted', 'in_review', 'information_pending'], true))) {
|
||||
$actions[] = 'request_revision';
|
||||
}
|
||||
}
|
||||
if ($this->access->scoped($actor, 'validator', $record->organization_id, $record->opportunity_category)
|
||||
&& $this->access->operational($actor, 'validate', $record->id) && ! $this->access->conflict($actor, $record)) {
|
||||
if (in_array($state, ['submitted', 'in_review'], true) && $this->responsibility($actor, $record, 'validation_coordinator')) {
|
||||
$actions[] = 'assign_review';
|
||||
}
|
||||
$assigned = data_get($record->workflow_context, 'assignment.assignee_user_id');
|
||||
if ($assigned === null || (int) $assigned === $actor->id) {
|
||||
if ($state === 'submitted') {
|
||||
$actions[] = 'start_review';
|
||||
}
|
||||
if ($state === 'in_review') {
|
||||
array_push($actions, 'request_correction', 'reject', 'approve');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $actions;
|
||||
}
|
||||
|
||||
public function reauthenticationRequired(User $actor, Opportunity $record): bool
|
||||
{
|
||||
return ! $this->access->mfa($actor)
|
||||
@@ -178,6 +220,7 @@ class OpportunityProfileWorkflow
|
||||
abort_unless(hash_equals($this->management->etag($record), $data['etag']), 409, 'Versão alterada.');
|
||||
$this->authorizeAction($actor, $record, $action);
|
||||
$context = $record->workflow_context ?? [];
|
||||
$offer = $record->isPrivateOfferPreparation();
|
||||
if ($action === 'submit' && app(PortalEditorialWorkflow::class)->available()) {
|
||||
$context['editorial_version_binding'] = 'phase1-exact-v1';
|
||||
$record->workflow_context = $context;
|
||||
@@ -191,7 +234,8 @@ class OpportunityProfileWorkflow
|
||||
'review_gate' => ['submitted', 'in_review', 'information_pending', 'approved'],
|
||||
'publish' => ['approved'], 'suspend' => [$state],
|
||||
'assign_publication' => ['approved'],
|
||||
'request_revision' => ['approved', 'published', 'suspended'],
|
||||
'request_revision' => $offer ? (($context['offer_operation']['status'] ?? null) === 'revalidation'
|
||||
? ['draft', 'needs_information', 'submitted', 'in_review', 'information_pending', 'approved', 'rejected'] : ['approved', 'rejected']) : ['approved', 'published', 'suspended'],
|
||||
'archive' => ['draft', 'submitted', 'in_review', 'information_pending', 'needs_information', 'approved', 'rejected', 'published', 'suspended'],
|
||||
'request_archive' => ['draft', 'submitted', 'in_review', 'information_pending', 'needs_information', 'approved', 'published', 'suspended'],
|
||||
'appeal', 'appeal_uphold', 'appeal_reopen' => ['rejected'],
|
||||
@@ -205,11 +249,35 @@ class OpportunityProfileWorkflow
|
||||
}
|
||||
$next = $state;
|
||||
$snapshot = [];
|
||||
if ($action === 'submit' && $record->opportunity_category === 'land_rights') {
|
||||
if ($offer) {
|
||||
$snapshot = in_array($action, ['request_revision', 'request_correction'], true)
|
||||
? ($context['submission'] ?? []) : app(PrivateOfferPreparation::class)->snapshot($record);
|
||||
// Explicit first review of an immutable phase-1 submission; no writes on read.
|
||||
if (in_array($action, ['start_review', 'assign_review'], true) && empty($context['submission'])) {
|
||||
abort_unless(($context['private_offer_preparation']['version'] ?? null) === 1
|
||||
&& $record->management_version === (int) ($context['private_offer_preparation']['submitted_revision'] ?? -2) + 1, 409, 'Reconcilie a submissão privada anterior.');
|
||||
$snapshot['record_version'] = $context['private_offer_preparation']['submitted_revision'];
|
||||
$context['submission'] = [...$snapshot, 'actor_id' => $context['private_offer_preparation']['submitted_by'], 'at' => $context['private_offer_preparation']['submitted_at']];
|
||||
$context['phase1_binding'] = ['bound_at' => $decision['at'], 'bound_by' => $actor->id, 'hash' => $snapshot['hash'], 'original_reference_fingerprint' => 'not_recorded'];
|
||||
$context['review_cycle'] = 1;
|
||||
$decision['review_cycle'] = 1;
|
||||
$context['phase1_submission'] = $context['private_offer_preparation'];
|
||||
$record->first_submitted_at ??= $context['private_offer_preparation']['submitted_at'];
|
||||
$record->management_closed_once = true;
|
||||
}
|
||||
if ($action !== 'submit') {
|
||||
abort_unless(hash_equals($context['submission']['hash'] ?? '', $snapshot['hash']), 409, 'A oferta ou as referências mudaram; prepare nova revisão antes da decisão.');
|
||||
}
|
||||
$decision['hash'] = $snapshot['hash'];
|
||||
$decision['opportunity_version'] = $record->management_version;
|
||||
$decision['submitted_revision'] = $action === 'submit' ? $snapshot['record_version'] : ($context['submission']['record_version'] ?? $record->management_version);
|
||||
$record->workflow_context = $context;
|
||||
}
|
||||
if (! $offer && $action === 'submit' && $record->opportunity_category === 'land_rights') {
|
||||
// Serialize the existing JSON-backed §27 uniqueness check across records.
|
||||
DB::select('SELECT pg_advisory_xact_lock(111026,270000)');
|
||||
}
|
||||
if (in_array($action, ['submit', 'review_gate', 'pending_opinion', 'respond_opinion', 'approve', 'publish'], true)) {
|
||||
if (! $offer && in_array($action, ['submit', 'review_gate', 'pending_opinion', 'respond_opinion', 'approve', 'publish'], true)) {
|
||||
$blockers = $this->contract->deliveryBlockers($this->management->values($record)['canonical'] ?? []);
|
||||
try {
|
||||
$snapshot = $this->contract->validateSubmission($record);
|
||||
@@ -228,6 +296,12 @@ class OpportunityProfileWorkflow
|
||||
}
|
||||
}
|
||||
if ($action === 'submit') {
|
||||
if ($offer && isset($context['submission'])) {
|
||||
$context['submission_history'][] = ['submission' => $context['submission'], 'approval' => $context['approval'] ?? null, 'rejection' => $context['rejection'] ?? null];
|
||||
}
|
||||
if ($offer) {
|
||||
unset($context['rejection'], $context['correction_items']);
|
||||
}
|
||||
// New review attempt, even when the canonical content is unchanged.
|
||||
// Historical decisions/tasks and the content revision keep their IDs.
|
||||
$context['review_cycle'] = max((int) ($context['review_cycle'] ?? 0), $this->submissionCount($context)) + 1;
|
||||
@@ -239,7 +313,7 @@ class OpportunityProfileWorkflow
|
||||
}
|
||||
}
|
||||
$baseline = $context['review_baseline'] ?? $context['submission'] ?? [];
|
||||
$materiality = app(OpportunityReviewMateriality::class)->reconcile($baseline, $snapshot, $context['reviews'] ?? [], $this->contract->gates($record), ! empty($context['force_review']), $strictEditorial);
|
||||
$materiality = app(OpportunityReviewMateriality::class)->reconcile($baseline, $snapshot, $context['reviews'] ?? [], $offer ? [] : $this->contract->gates($record), ! empty($context['force_review']), $strictEditorial);
|
||||
$context['materiality'] = array_diff_key($materiality, ['reviews' => true]);
|
||||
$context['reviews'] = $materiality['reviews'];
|
||||
app(OpportunityReviewConditions::class)->reconcile($context, $snapshot, $materiality['reopened']);
|
||||
@@ -270,6 +344,11 @@ class OpportunityProfileWorkflow
|
||||
}
|
||||
$context['review_task_id'] ??= $this->tasks->ensure($record, 'opportunity_review', 'Validação da oportunidade '.$record->reference);
|
||||
$this->tasks->assign($record, $context['review_task_id'], $subject);
|
||||
if ($offer) {
|
||||
$decision['previous_assignee_user_id'] = $context['assignment']['assignee_user_id'] ?? null;
|
||||
$decision['assignee_user_id'] = $subject->id;
|
||||
$context['assignment_history'][] = $decision;
|
||||
}
|
||||
$context['assignment'] = [...$decision, 'assignee_user_id' => $subject->id];
|
||||
$next = 'in_review';
|
||||
} elseif ($action === 'review_gate') {
|
||||
@@ -292,7 +371,7 @@ class OpportunityProfileWorkflow
|
||||
app(OpportunityReviewConditions::class)->authorize($context, $snapshot, 'approve');
|
||||
abort_if(count(array_filter($context['opinions'] ?? [], fn ($o) => ($o['hash'] ?? '') === $snapshot['hash'] && ($o['status'] ?? '') === 'pending')) > 0, 422, 'Parecer da versão atual ainda pendente.');
|
||||
abort_if(count(array_filter($context['opinions'] ?? [], fn ($o) => ($o['hash'] ?? '') === $snapshot['hash'] && ($o['response']['decision'] ?? '') === 'rejected')) > 0, 422, 'Parecer desfavorável exige correção e nova versão.');
|
||||
foreach ($this->contract->gates($record) as $gate) {
|
||||
foreach ($offer ? [] : $this->contract->gates($record) as $gate) {
|
||||
$review = $context['reviews'][$gate] ?? [];
|
||||
abort_unless(($review['decision'] ?? null) === 'approved' && (($review['hash'] ?? '') === $snapshot['hash'] || ($review['accepted_submission_hash'] ?? '') === $snapshot['hash']), 422, 'Gate '.$gate.' pendente, rejeitado ou de outra versão.');
|
||||
abort_unless(app(OpportunityReviewMateriality::class)->matches($snapshot, $gate, $review, $strictEditorial), 409, 'Âmbito ou versão da revisão alterado.');
|
||||
@@ -302,9 +381,14 @@ class OpportunityProfileWorkflow
|
||||
&& $this->responsibility($reviewer, $record, preg_match('/legal|registry|authority|confidentiality|license/', $gate) === 1 ? 'legal_reviewer' : 'technical_reviewer'), 403, 'Revisor revogado, sem responsabilidade ou com conflito.');
|
||||
}
|
||||
$context['approval'] = [...$decision, 'hash' => $snapshot['hash'], 'gates' => $context['reviews'] ?? []];
|
||||
if ($offer) {
|
||||
$context['offer_operation'] = app(PrivateOfferOperations::class)->afterApproval($context);
|
||||
}
|
||||
$this->tasks->complete($context['review_task_id'] ?? null, $data['reason']);
|
||||
$record->workflow_context = $context;
|
||||
$context['publication_task_id'] = $this->tasks->ensure($record, 'opportunity_publication', 'Preparação editorial '.$record->reference);
|
||||
if (! $offer) {
|
||||
$context['publication_task_id'] = $this->tasks->ensure($record, 'opportunity_publication', 'Preparação editorial '.$record->reference);
|
||||
}
|
||||
$next = 'approved';
|
||||
} elseif ($action === 'assign_publication') {
|
||||
$subject = User::whereKey($data['assignee_user_id'] ?? 0)->firstOrFail();
|
||||
@@ -414,6 +498,9 @@ class OpportunityProfileWorkflow
|
||||
$next = count(array_filter($context['opinions'], fn ($o) => ($o['hash'] ?? '') === $snapshot['hash'] && ($o['status'] ?? '') === 'pending')) > 0 ? 'information_pending' : 'in_review';
|
||||
} elseif ($action === 'request_correction') {
|
||||
abort_unless($data['correction_items'] ?? [], 422, 'Indique campos e documentos a corrigir.');
|
||||
if ($offer) {
|
||||
abort_if(array_diff($data['correction_items'], ['title', 'description', 'opportunity_category', 'private_offer', 'contact', 'location']) !== [], 422, 'Seleccione campos desta oferta.');
|
||||
}
|
||||
$this->tasks->complete($context['review_task_id'] ?? null, $data['reason']);
|
||||
$context['correction_items'] = $data['correction_items'];
|
||||
$context['correction_cycles'] = ($context['correction_cycles'] ?? 0) + 1;
|
||||
@@ -423,7 +510,7 @@ class OpportunityProfileWorkflow
|
||||
} elseif ($action === 'request_revision' || $action === 'appeal_reopen') {
|
||||
$this->tasks->complete($context['review_task_id'] ?? null, $data['reason']);
|
||||
$this->tasks->complete($context['publication_task_id'] ?? null, $data['reason']);
|
||||
$context['decision_history'][] = ['approval' => $context['approval'] ?? null, 'reviews' => $context['reviews'] ?? null, 'submission' => $context['submission'] ?? null];
|
||||
$context['decision_history'][] = ['approval' => $context['approval'] ?? null, 'reviews' => $context['reviews'] ?? null, 'submission' => $context['submission'] ?? null, ...($offer ? ['rejection' => $context['rejection'] ?? null] : [])];
|
||||
$context['review_baseline'] = $context['submission'] ?? [];
|
||||
$context['force_review'] = $action === 'appeal_reopen' || $state === 'suspended';
|
||||
unset($context['approval']);
|
||||
@@ -476,7 +563,14 @@ class OpportunityProfileWorkflow
|
||||
$context['working_state'] = $next;
|
||||
$context['last_decision'] = $decision;
|
||||
$context['events'][] = [...$decision, 'from' => $state, 'to' => $next];
|
||||
if ($record->status !== 'published' || in_array($action, ['suspend', 'archive'], true)) {
|
||||
if ($offer) {
|
||||
$context['private_offer_preparation'] = [...$context['private_offer_preparation'], 'version' => 2, 'state' => $next];
|
||||
if ($action === 'submit') {
|
||||
$context['private_offer_preparation'] = [...$context['private_offer_preparation'], 'submitted_by' => $actor->id,
|
||||
'submitted_at' => $decision['at'], 'submitted_revision' => $snapshot['record_version']];
|
||||
}
|
||||
}
|
||||
if (! $offer && ($record->status !== 'published' || in_array($action, ['suspend', 'archive'], true))) {
|
||||
$record->status = $next;
|
||||
}
|
||||
$record->management_version++;
|
||||
@@ -484,7 +578,12 @@ class OpportunityProfileWorkflow
|
||||
$record->save();
|
||||
app(PortalEditorialWorkflow::class)->notifyOpportunity($actor, $record, $action);
|
||||
$this->access->audit($actor, 'workflow_'.$action, ['record_id' => $record->id, 'before' => $before,
|
||||
'after' => ['status' => $next, 'publication_status' => $record->status, 'version' => $record->management_version], 'reason' => $data['reason']]);
|
||||
'after' => ['status' => $next, 'publication_status' => $record->status, 'version' => $record->management_version], 'reason' => $data['reason'],
|
||||
...($offer ? ['decision' => $decision, 'submission_hash' => $context['submission']['hash'] ?? null,
|
||||
'snapshot' => Crypt::encryptString(json_encode($snapshot, JSON_THROW_ON_ERROR))] : [])]);
|
||||
if ($offer && $action === 'submit') {
|
||||
$this->access->audit($actor, 'private_offer_submitted', ['record_id' => $record->id, 'version' => $record->management_version, 'public' => false]);
|
||||
}
|
||||
|
||||
return $record;
|
||||
});
|
||||
@@ -520,6 +619,9 @@ class OpportunityProfileWorkflow
|
||||
|
||||
private function authorizeAction(User $actor, Opportunity $record, string $action): void
|
||||
{
|
||||
if ($record->isPrivateOfferPreparation()) {
|
||||
abort_unless(in_array($action, $this->actions($actor, $record), true), 403, 'Operação editorial não autorizada para esta oferta privada.');
|
||||
}
|
||||
$this->access->authorize($actor, 'view', $record);
|
||||
$promoter = $this->access->promoterRecordScope($actor, $record)
|
||||
&& ($record->organization_id !== null || (int) $record->owner_user_id === $actor->id);
|
||||
|
||||
@@ -22,7 +22,7 @@ final class OpportunityPromotion
|
||||
public function allowed(User $actor, Opportunity $record): bool
|
||||
{
|
||||
$access = app(PortalProfileAccess::class);
|
||||
if ($record->trashed() || $record->owner_user_id === null || ! $access->canView($actor, $record)
|
||||
if ($record->isPrivateOfferPreparation() || $record->trashed() || $record->owner_user_id === null || ! $access->canView($actor, $record)
|
||||
|| $access->has($actor, 'auditor') || in_array($record->status, ['archived', 'rejected'], true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ class OpportunityResources
|
||||
}
|
||||
abort_if(! $id && DB::table($table)->where('id', $resourceId)->exists(), 409, 'Esta criação já foi registada; recupere o recurso existente.');
|
||||
$active = DB::table($table)->where('opportunity_id', $opportunityId)->whereNull('deleted_at');
|
||||
abort_if(! $id && $active->count() >= ($kind === 'locations' ? 50 : ($kind === 'images' ? 20 : 100)), 422, 'Limite de recursos atingido.');
|
||||
abort_if(! $id && $active->count() >= $this->limit($opportunity, $kind), 422, 'Limite de recursos atingido.');
|
||||
if ($kind === 'locations') {
|
||||
$others = (clone $active)->when($id, fn ($q) => $q->where('id', '!=', $id))->get();
|
||||
foreach ($others as $other) {
|
||||
@@ -172,7 +172,7 @@ class OpportunityResources
|
||||
abort_if(($data['action'] === 'delete') === ($record->deleted_at !== null), 409, 'Estado já alterado.');
|
||||
$payload = json_decode($record->payload, true);
|
||||
if ($data['action'] === 'restore') {
|
||||
abort_if(DB::table($table)->where('opportunity_id', $opportunityId)->whereNull('deleted_at')->count() >= ($kind === 'locations' ? 50 : ($kind === 'images' ? 20 : 100)), 422, 'Limite de recursos ativos atingido; reconcilie antes de restaurar.');
|
||||
abort_if(DB::table($table)->where('opportunity_id', $opportunityId)->whereNull('deleted_at')->count() >= $this->limit($opportunity, $kind), 422, 'Limite de recursos ativos atingido; reconcilie antes de restaurar.');
|
||||
$this->validatePayload($kind, $payload, $opportunityId);
|
||||
// Restoration never silently reclaims a primary/cover/order occupied by another resource.
|
||||
if ($kind === 'images') {
|
||||
@@ -221,12 +221,22 @@ class OpportunityResources
|
||||
*/
|
||||
private function validatePayload(string $kind, array $payload, int $opportunityId): array
|
||||
{
|
||||
$offer = Opportunity::findOrFail($opportunityId);
|
||||
if ($offer->isPrivateOfferPreparation()) {
|
||||
Validator::make($payload, ['visibility' => 'required|in:internal',
|
||||
...($kind === 'images' ? ['digitally_edited' => 'required|boolean', 'edit_description' => 'nullable|required_if:digitally_edited,true|string|max:1000'] : []),
|
||||
...($kind === 'documents' ? ['category' => 'required|in:real_estate_proof', 'classification' => 'required|in:restricted'] : [])])->validate();
|
||||
abort_if($kind === 'documents' && $offer->opportunity_category !== 'real_estate', 422, 'Este percurso aceita comprovativos imobiliários.');
|
||||
}
|
||||
$rules = ['title' => 'nullable|string|max:120', 'source_reference' => 'nullable|string|max:2000', 'source_date' => 'nullable|date_format:Y-m-d', 'visibility' => 'nullable|in:internal,authenticated,public'];
|
||||
if ($kind === 'locations') {
|
||||
$rules += ['country_code' => ['nullable', app(CountryCatalogue::class)->rule()]];
|
||||
$rules += ['province_id' => 'nullable|uuid', 'district_id' => 'nullable|uuid', 'admin_post_id' => 'nullable|uuid', 'locality_id' => 'nullable|uuid', 'role' => 'nullable|in:headquarters,production,raw_material,storage,logistics,market,support,other', 'role_description' => 'nullable|string|max:1000', 'operational_unit' => 'nullable|string|max:120', 'is_primary' => 'nullable|boolean', 'address' => 'nullable|string|max:300', 'geometry' => 'nullable|array', 'area_value' => 'nullable|numeric|gt:0', 'area_unit' => ['nullable', Rule::in(self::AREA_UNITS)], 'allocation_percentage' => 'nullable|numeric|between:0,100', 'territorial_purpose' => 'nullable|in:implementation,context,impact,coverage', 'multi_district_rationale' => 'nullable|string|max:1000'];
|
||||
} elseif ($kind === 'images') {
|
||||
$rules += ['role' => 'nullable|in:cover,gallery,location,asset,product,plan,rendering,evidence,other', 'role_description' => 'nullable|string|max:1000', 'is_cover' => 'nullable|boolean', 'caption' => 'nullable|string|max:300', 'alt_text' => 'nullable|string|max:250', 'location_id' => ['nullable', 'uuid', Rule::exists('opportunity_location_drafts', 'id')->where('opportunity_id', $opportunityId)->whereNull('deleted_at')], 'captured_at' => 'nullable|date_format:Y-m-d|before_or_equal:today', 'author_source' => 'nullable|string|max:150', 'rights_status' => 'nullable|in:owned,licensed,authorized,public_domain', 'rights_document_id' => ['nullable', 'uuid', Rule::exists('opportunity_documents', 'id')->where('opportunity_id', $opportunityId)->whereNull('deleted_at')], 'rights_valid_until' => 'nullable|date_format:Y-m-d', 'sort_order' => 'nullable|integer|min:0'];
|
||||
if ($offer->isPrivateOfferPreparation()) {
|
||||
$rules += ['digitally_edited' => 'boolean', 'edit_description' => 'nullable|string|max:1000'];
|
||||
}
|
||||
} else {
|
||||
$rules += ['category' => 'nullable|string|max:100', 'issuer' => 'nullable|string|max:255', 'edition' => 'nullable|string|max:120', 'valid_until' => 'nullable|date_format:Y-m-d', 'classification' => 'nullable|in:public,internal,confidential,restricted', 'section' => 'nullable|string|max:255', 'supported_statement' => 'nullable|string|max:2000'];
|
||||
}
|
||||
@@ -257,6 +267,11 @@ class OpportunityResources
|
||||
return $data;
|
||||
}
|
||||
|
||||
private function limit(Opportunity $record, string $kind): int
|
||||
{
|
||||
return $kind === 'locations' ? 50 : ($kind === 'images' ? ($record->isPrivateOfferPreparation() ? 6 : 20) : 100);
|
||||
}
|
||||
|
||||
/** @return array{string,string} */
|
||||
private function store(string $kind, UploadedFile $file): array
|
||||
{
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Jobs\NotifyPrivateOfferDecision;
|
||||
use App\Models\DevelopmentCorridor;
|
||||
use App\Models\EconomicCluster;
|
||||
use App\Models\InvestmentDossier;
|
||||
@@ -237,12 +238,50 @@ final class PortalEditorialWorkflow
|
||||
}
|
||||
$context = $record->workflow_context ?? [];
|
||||
$recipients = array_unique(array_filter([$record->owner_user_id, $context['assignment']['assignee_user_id'] ?? null, $context['publication_assignment']['assignee_user_id'] ?? null]));
|
||||
$profiles = app(PortalProfileAccess::class);
|
||||
if ($record->isPrivateOfferPreparation() && $action === 'assign_review' && isset($context['last_decision']['previous_assignee_user_id'])) {
|
||||
$recipients[] = $context['last_decision']['previous_assignee_user_id'];
|
||||
}
|
||||
if ($record->isPrivateOfferPreparation() && $action === 'submit') {
|
||||
$coordinators = DB::table('portal_role_assignments')->where('role', 'validator')->whereNull('revoked_at')
|
||||
->where('starts_at', '<=', now())->where(fn ($q) => $q->whereNull('expires_at')->orWhere('expires_at', '>', now()))
|
||||
->whereJsonContains('responsibilities', 'validation_coordinator')->pluck('user_id');
|
||||
foreach (User::whereIn('id', $coordinators)->get() as $candidate) {
|
||||
if ($profiles->scoped($candidate, 'validator', $record->organization_id, $record->opportunity_category)
|
||||
&& $profiles->operational($candidate, 'validate', $record->id) && ! $profiles->conflict($candidate, $record)) {
|
||||
$recipients[] = $candidate->id;
|
||||
}
|
||||
}
|
||||
}
|
||||
$recipients = array_unique($recipients);
|
||||
foreach ($recipients as $recipient) {
|
||||
if ((int) $recipient === $actor->id) {
|
||||
continue;
|
||||
}
|
||||
DB::table('portal_editorial_notifications')->insertOrIgnore(['user_id' => $recipient, 'edition_id' => null, 'opportunity_id' => $record->id, 'opportunity_version' => $record->management_version,
|
||||
if ($record->isPrivateOfferPreparation()) {
|
||||
$candidate = User::query()->whereKey($recipient)->first();
|
||||
if (! $candidate || ! $profiles->canView($candidate, $record) || ! $this->canEnter($candidate)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
$inserted = DB::table('portal_editorial_notifications')->insertOrIgnore(['user_id' => $recipient, 'edition_id' => null, 'opportunity_id' => $record->id, 'opportunity_version' => $record->management_version,
|
||||
'content_hash' => $context['submission']['hash'] ?? $context['approval']['hash'] ?? null, 'action' => $action, 'created_at' => now()]);
|
||||
if ($inserted && $record->isPrivateOfferPreparation()) {
|
||||
$notice = DB::table('portal_editorial_notifications')->where('user_id', $recipient)->where('opportunity_id', $record->id)
|
||||
->where('opportunity_version', $record->management_version)->where('action', $action)->first();
|
||||
if ($notice) {
|
||||
DB::afterCommit(function () use ($notice, $actor, $record): void {
|
||||
try {
|
||||
NotifyPrivateOfferDecision::dispatch($notice->id);
|
||||
} catch (\Throwable $error) {
|
||||
// A committed decision must not become an uncertain HTTP failure.
|
||||
app(PortalProfileAccess::class)->audit($actor, 'offer_notification_delivery_failed', [
|
||||
'record_id' => $record->id, 'notice_id' => $notice->id, 'error_type' => get_class($error),
|
||||
]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -334,6 +373,9 @@ final class PortalEditorialWorkflow
|
||||
}
|
||||
$edition = $notice->edition_id ? DB::table('portal_editorial_editions')->where('id', $notice->edition_id)->first() : null;
|
||||
$subjectHref = ($edition->subject_kind ?? null) ? '/management/editorial/subjects/'.$edition->subject_kind.'/'.$edition->subject_key.'?edition='.$edition->id : null;
|
||||
if ($notice->opportunity_id !== null && Opportunity::query()->whereKey($notice->opportunity_id)->first()?->isPrivateOfferPreparation()) {
|
||||
$subjectHref = '/minhas-oportunidades/ofertas/'.$notice->opportunity_id;
|
||||
}
|
||||
$result[] = ['id' => $notice->id, 'action' => $notice->action, 'read_at' => $notice->read_at,
|
||||
'href' => $subjectHref ?? ($notice->opportunity_id !== null ? '/minhas-oportunidades/'.$notice->opportunity_id : (($edition->demographic_import_id ?? null) ? '/management/editorial/demography/'.$edition->demographic_import_id : (($edition->dossier_id ?? null) ? '/management/editorial/portfolio/'.$edition->dossier_id : '/management/editorial/resources/'.$edition->resource_id)).'?edition='.$notice->edition_id), 'reference' => $notice->opportunity_id !== null ? 'Revisão '.$notice->opportunity_version : 'Edição '.$edition->revision_no];
|
||||
if (count($result) === 50) {
|
||||
|
||||
@@ -241,6 +241,9 @@ class PortalProfileAccess
|
||||
|
||||
public function canEdit(User $actor, Opportunity $record, ?string $newType = null): bool
|
||||
{
|
||||
if ($record->isPrivateOfferPreparation() && ! in_array($this->state($record), ['draft', 'needs_information'], true)) {
|
||||
return false;
|
||||
}
|
||||
if ($record->owner_user_id === null) {
|
||||
return $this->legacyWriteAllowed($actor);
|
||||
}
|
||||
@@ -264,6 +267,10 @@ class PortalProfileAccess
|
||||
|
||||
public function state(Opportunity $record): string
|
||||
{
|
||||
if ($record->isPrivateOfferPreparation()) {
|
||||
return data_get($record->workflow_context, 'private_offer_preparation.state', 'draft');
|
||||
}
|
||||
|
||||
return $record->workflow_context['working_state'] ?? $record->status;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Opportunity;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
|
||||
|
||||
/** The same opportunity owns consent, grants and history. No automatic publication. */
|
||||
final class PrivateOfferConfidentiality
|
||||
{
|
||||
public function internal(User $actor, Opportunity $record): bool
|
||||
{
|
||||
$access = app(PortalProfileAccess::class);
|
||||
|
||||
return $access->canView($actor, $record) && $access->mfa($actor) && ! $access->has($actor, 'auditor') && ! $access->conflict($actor, $record)
|
||||
&& $access->scoped($actor, 'validator', $record->organization_id, $record->opportunity_category);
|
||||
}
|
||||
|
||||
public function seller(User $actor, Opportunity $record): bool
|
||||
{
|
||||
$access = app(PortalProfileAccess::class);
|
||||
|
||||
return (int) $record->owner_user_id === $actor->id && $access->promoterRecordScope($actor, $record)
|
||||
&& $access->canCreate($actor, $record->organization_id, $record->opportunity_category) && $access->operational($actor, 'edit', $record->id);
|
||||
}
|
||||
|
||||
/** @return array<string,mixed>|null */
|
||||
public function nda(): ?array
|
||||
{
|
||||
$definition = config('private_offers.nda');
|
||||
if (! is_array($definition) || empty($definition['approval_reference']) || empty($definition['document_id']) || empty($definition['version']) || empty($definition['sha256'])) {
|
||||
return null;
|
||||
}
|
||||
$doc = DB::table('documents')->where('id', $definition['document_id'])->whereNull('deleted_at')->first();
|
||||
if (! $doc || $doc->status !== 'active' || $doc->malware_scan_status !== 'clean' || (int) $doc->version_no !== (int) $definition['version']
|
||||
|| ! hash_equals(trim($doc->sha256), $definition['sha256']) || ! hash_equals(trim($doc->sha256), trim((string) $doc->malware_scanned_sha256))) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
app(VerifiedDocumentBinary::class)->respond($doc);
|
||||
} catch (HttpExceptionInterface $error) {
|
||||
return null;
|
||||
}
|
||||
$result = ['document_id' => $doc->id, 'version' => (int) $doc->version_no, 'sha256' => trim($doc->sha256), 'approval_reference' => $definition['approval_reference']];
|
||||
|
||||
return [...$result, 'fingerprint' => hash('sha256', json_encode($result, JSON_THROW_ON_ERROR))];
|
||||
}
|
||||
|
||||
private function company(int $id, bool $lock = false): Opportunity
|
||||
{
|
||||
$record = Opportunity::query()->when($lock, fn ($q) => $q->lockForUpdate())->findOrFail($id);
|
||||
abort_unless($record->isPrivateOfferPreparation() && $record->opportunity_category === 'business_sale', 404);
|
||||
|
||||
return $record;
|
||||
}
|
||||
|
||||
private function approvedHash(Opportunity $record): string
|
||||
{
|
||||
abort_unless(app(OfferAvailability::class)->record($record)['state'] === 'available', 409, 'A oferta não está disponível.');
|
||||
$hash = $record->workflow_context['approval']['hash'] ?? null;
|
||||
abort_unless(is_string($hash) && app(PortalProfileAccess::class)->state($record) === 'approved', 409, 'Esta versão não tem aprovação editorial actual.');
|
||||
try {
|
||||
$snapshot = app(PrivateOfferPreparation::class)->snapshot($record);
|
||||
} catch (ValidationException $error) {
|
||||
abort(409, 'A versão ou os suportes exigem reconferência editorial.');
|
||||
}
|
||||
abort_unless(hash_equals($hash, $snapshot['hash']), 409, 'A versão mudou; é necessária reconferência editorial.');
|
||||
|
||||
return $hash;
|
||||
}
|
||||
|
||||
/** PostgreSQL jsonb reorders object keys; identity is the version and checksum, not array order.
|
||||
* @param array<string,mixed> $nda */
|
||||
private function sameNda(mixed $accepted, array $nda): bool
|
||||
{
|
||||
if (! is_array($accepted)) {
|
||||
return false;
|
||||
}
|
||||
foreach (['document_id', 'version', 'sha256', 'approval_reference', 'fingerprint'] as $key) {
|
||||
if (($accepted[$key] ?? null) !== $nda[$key]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
public function overview(User $actor, int $id): array
|
||||
{
|
||||
$record = $this->company($id);
|
||||
// An interested party learns no private title, contacts, documents or financial values.
|
||||
$seller = $this->seller($actor, $record);
|
||||
$nda = $this->nda();
|
||||
$context = $record->workflow_context['offer_confidentiality'] ?? [];
|
||||
$currentHash = null;
|
||||
try {
|
||||
$currentHash = $this->approvedHash($record);
|
||||
} catch (HttpExceptionInterface $error) {
|
||||
// A previous acceptance remains withdrawable even if the offer was revised.
|
||||
abort_unless($seller || isset($context['acceptances'][(string) $actor->id]), 404);
|
||||
}
|
||||
$history = array_values(array_filter($context['events'] ?? [], fn ($event) => $seller || $event['interested_user_id'] === $actor->id));
|
||||
|
||||
return ['offer_id' => $id, 'nda' => $nda, 'nda_pending' => $nda === null, 'version_hash' => $record->workflow_context['approval']['hash'] ?? null,
|
||||
'etag' => $seller ? app(OpportunityManagement::class)->etag($record) : null,
|
||||
'can_accept' => ! $seller && $nda !== null && $currentHash !== null && ! app(PortalProfileAccess::class)->conflict($actor, $record),
|
||||
'can_authorize' => $seller && $nda !== null && $currentHash !== null,
|
||||
'seller' => $seller, 'requests' => $seller ? array_values($context['acceptances'] ?? []) : [],
|
||||
'grants' => $seller ? array_values($context['grants'] ?? []) : [],
|
||||
'history' => array_map(fn ($event) => array_intersect_key($event, array_flip(['id', 'action', 'actor_id', 'at', 'version_hash', 'nda'])), $history),
|
||||
'acceptance' => $context['acceptances'][(string) $actor->id] ?? null];
|
||||
}
|
||||
|
||||
/** @param array<string,mixed> $input
|
||||
* @return array<string,mixed> */
|
||||
public function act(User $actor, int $id, array $input): array
|
||||
{
|
||||
$data = validator($input, ['action' => 'required|in:accept,authorize,revoke,withdraw', 'version_hash' => 'required|string|size:64',
|
||||
'nda_sha256' => 'required_unless:action,revoke,withdraw|string|size:64', 'acceptance_id' => 'required_if:action,authorize|uuid',
|
||||
'nda_fingerprint' => 'required_if:action,accept,authorize|string|size:64',
|
||||
'interested_user_id' => 'required_if:action,authorize,revoke|integer|min:1', 'confirm' => 'required|accepted',
|
||||
'reason' => ['nullable', 'required_if:action,authorize,revoke', 'string', 'max:1000', ...(in_array($input['action'] ?? null, ['authorize', 'revoke'], true) ? ['min:20'] : [])]])->validate();
|
||||
|
||||
return DB::transaction(function () use ($actor, $id, $data) {
|
||||
$record = $this->company($id, true);
|
||||
$context = $record->workflow_context;
|
||||
$privacy = $context['offer_confidentiality'] ?? ['acceptances' => [], 'grants' => [], 'events' => []];
|
||||
$action = $data['action'];
|
||||
$target = in_array($action, ['authorize', 'revoke'], true) ? (string) $data['interested_user_id'] : (string) $actor->id;
|
||||
$before = ['acceptance' => $privacy['acceptances'][$target] ?? null, 'grant' => $privacy['grants'][$target] ?? null];
|
||||
if (in_array($action, ['authorize', 'revoke'], true)) {
|
||||
abort_unless($this->seller($actor, $record), 403);
|
||||
} elseif ($action === 'accept') {
|
||||
abort_if($this->seller($actor, $record) || app(PortalProfileAccess::class)->conflict($actor, $record), 403);
|
||||
}
|
||||
if (in_array($action, ['accept', 'authorize'], true)) {
|
||||
$nda = $this->nda();
|
||||
abort_unless($nda !== null, 409, 'O exemplar aprovado do NDA ainda não foi disponibilizado.');
|
||||
abort_unless(hash_equals($this->approvedHash($record), $data['version_hash']) && hash_equals($nda['sha256'], $data['nda_sha256'])
|
||||
&& hash_equals($nda['fingerprint'], $data['nda_fingerprint']), 409);
|
||||
$subject = User::findOrFail((int) $target);
|
||||
abort_unless($subject->hasVerifiedEmail() && ! app(PortalProfileAccess::class)->conflict($subject, $record), 403);
|
||||
if ($action === 'accept') {
|
||||
$privacy['acceptances'][$target] = ['id' => (string) Str::uuid(), 'actor_id' => $actor->id, 'at' => now()->toIso8601String(),
|
||||
'version_hash' => $data['version_hash'], 'nda' => $nda, 'access_revision' => (int) $subject->portal_access_revision];
|
||||
unset($privacy['grants'][$target]); // A fresh acceptance never reuses a previous seller decision.
|
||||
} else {
|
||||
$acceptance = $privacy['acceptances'][$target] ?? null;
|
||||
abort_unless($acceptance && $acceptance['id'] === $data['acceptance_id'] && $acceptance['version_hash'] === $data['version_hash']
|
||||
&& $this->sameNda($acceptance['nda'], $nda) && $acceptance['access_revision'] === (int) $subject->portal_access_revision, 409);
|
||||
$privacy['grants'][$target] = ['interested_user_id' => (int) $target, 'seller_id' => $actor->id, 'at' => now()->toIso8601String(),
|
||||
'version_hash' => $data['version_hash'], 'acceptance_id' => $acceptance['id'], 'nda' => $nda];
|
||||
}
|
||||
} else {
|
||||
$previous = $privacy[$action === 'withdraw' ? 'acceptances' : 'grants'][$target] ?? null;
|
||||
abort_unless($previous && $previous['version_hash'] === $data['version_hash'], 409);
|
||||
unset($privacy['grants'][$target]);
|
||||
if ($action === 'withdraw') {
|
||||
unset($privacy['acceptances'][$target]);
|
||||
}
|
||||
}
|
||||
$event = ['id' => (string) Str::uuid(), 'action' => $action, 'actor_id' => $actor->id, 'interested_user_id' => (int) $target,
|
||||
'version_hash' => $data['version_hash'], 'at' => now()->toIso8601String(), 'nda_sha256' => $data['nda_sha256'] ?? null,
|
||||
'nda' => $nda ?? ($before['acceptance']['nda'] ?? $before['grant']['nda'] ?? null)];
|
||||
$privacy['events'][] = [...$event, 'reason' => Crypt::encryptString($data['reason'] ?? ''), 'before' => $before,
|
||||
'after' => ['acceptance' => $privacy['acceptances'][$target] ?? null, 'grant' => $privacy['grants'][$target] ?? null]];
|
||||
$context['offer_confidentiality'] = $privacy;
|
||||
$record->workflow_context = $context;
|
||||
$record->management_version++;
|
||||
$record->save();
|
||||
app(PortalProfileAccess::class)->audit($actor, 'offer_confidentiality_'.$action, ['record_id' => $id, ...$event]);
|
||||
|
||||
return ['recorded' => true, 'event_id' => $event['id']];
|
||||
});
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
public function read(User $actor, int $id): array
|
||||
{
|
||||
return DB::transaction(function () use ($actor, $id) {
|
||||
$record = $this->company($id, true);
|
||||
$nda = $this->nda();
|
||||
$privacy = $record->workflow_context['offer_confidentiality'] ?? [];
|
||||
$accept = $privacy['acceptances'][(string) $actor->id] ?? null;
|
||||
$grant = $privacy['grants'][(string) $actor->id] ?? null;
|
||||
$hash = $this->approvedHash($record);
|
||||
$seller = User::find($record->owner_user_id);
|
||||
$subject = User::find($actor->id);
|
||||
$allowed = $nda && $accept && $grant && $seller && $subject && $this->seller($seller, $record) && $subject->hasVerifiedEmail()
|
||||
&& $accept['access_revision'] === (int) $subject->portal_access_revision && $grant['seller_id'] === $seller->id
|
||||
&& $grant['acceptance_id'] === $accept['id'] && $grant['version_hash'] === $hash && $accept['version_hash'] === $hash
|
||||
&& $this->sameNda($grant['nda'], $nda) && $this->sameNda($accept['nda'], $nda) && ! app(PortalProfileAccess::class)->conflict($actor, $record);
|
||||
app(PortalProfileAccess::class)->audit($actor, $allowed ? 'offer_confidential_access' : 'offer_confidential_denied', ['record_id' => $id, 'version_hash' => $hash,
|
||||
'nda' => $nda, 'acceptance_id' => $accept['id'] ?? null]);
|
||||
// Return the refusal after transaction commit so the denied-access audit survives.
|
||||
if (! $allowed) {
|
||||
return ['denied' => true];
|
||||
}
|
||||
|
||||
$business = $record->workflow_context['submission']['payload']['private_offer']['business'] ?? [];
|
||||
foreach (['sector_id' => 'sector', 'secondary_sector_id' => 'secondary_sector'] as $field => $label) {
|
||||
if (isset($business[$field])) {
|
||||
$business[$label] = DB::table('sectors')->where('id', $business[$field])->value('name');
|
||||
}
|
||||
unset($business[$field]);
|
||||
}
|
||||
|
||||
return ['offer_id' => $id, 'version_hash' => $hash, 'business' => $business];
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Opportunity;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
/** Optional commercial fields confirmed by Tempo for phase four; no title certification. */
|
||||
final class PrivateOfferDetails
|
||||
{
|
||||
/** @param array<string,mixed> $input
|
||||
* @return array<string,mixed> */
|
||||
public function validate(array $input): array
|
||||
{
|
||||
$bytes = file_get_contents(resource_path('data/investmoz-opportunity-modular.schema.json'));
|
||||
if ($bytes === false) {
|
||||
throw new \RuntimeException('Opportunity schema unavailable.');
|
||||
}
|
||||
$schema = json_decode($bytes, true, 512, JSON_THROW_ON_ERROR);
|
||||
$property = $schema['$defs']['realEstatePatch']['properties'];
|
||||
|
||||
return Validator::make($input, [
|
||||
'business' => 'sometimes|nullable|array:sector_id,secondary_sector_id,revenue,cash_flow,currency,reason,availability',
|
||||
'business.sector_id' => ['nullable', 'integer', Rule::exists('sectors', 'id')->where('is_active', true)],
|
||||
'business.secondary_sector_id' => ['nullable', 'integer', Rule::exists('sectors', 'id')->where('is_active', true)],
|
||||
'business.revenue' => ['nullable', 'string', 'regex:/^\d{1,16}(\.\d{1,2})?$/'],
|
||||
'business.cash_flow' => ['nullable', 'string', 'regex:/^-?\d{1,16}(\.\d{1,2})?$/'],
|
||||
'business.currency' => 'nullable|required_with:business.revenue,business.cash_flow|in:MZN,USD',
|
||||
'business.reason' => 'nullable|string|max:1000', 'business.availability' => 'nullable|in:available,sold',
|
||||
'property' => 'sometimes|nullable|array:transaction_type,property_type,land_area,built_area,area_unit,bedrooms,bathrooms,amenities,availability',
|
||||
'property.transaction_type' => 'nullable|in:sale,lease',
|
||||
'property.property_type' => ['nullable', Rule::in($property['property_type']['enum'])],
|
||||
'property.land_area' => 'nullable|numeric|gt:0', 'property.built_area' => 'nullable|numeric|gt:0',
|
||||
'property.area_unit' => ['nullable', Rule::in(OpportunityResources::AREA_UNITS)],
|
||||
'property.bedrooms' => 'nullable|integer|min:0|max:10000', 'property.bathrooms' => 'nullable|integer|min:0|max:10000',
|
||||
'property.amenities' => 'nullable|array|max:100', 'property.amenities.*' => 'string|max:250',
|
||||
'property.availability' => 'nullable|in:available,sold,leased',
|
||||
])->validate();
|
||||
}
|
||||
|
||||
/** @param array<string,mixed> $offer */
|
||||
public function assertCategory(array $offer, string $category): void
|
||||
{
|
||||
Validator::make($offer, [
|
||||
'business' => $category === 'business_sale' ? 'nullable|array' : 'prohibited',
|
||||
'property' => $category === 'real_estate' ? 'nullable|array' : 'prohibited',
|
||||
])->validate();
|
||||
}
|
||||
|
||||
/** Snapshot all active linked originals and metadata in the existing editorial version.
|
||||
* @return array<string,array<string,mixed>> */
|
||||
public function resources(Opportunity $record): array
|
||||
{
|
||||
$result = ['documents' => [], 'images' => [], 'locations' => []];
|
||||
foreach (array_keys($result) as $kind) {
|
||||
foreach (app(OpportunityResources::class)->listing($record->id, $kind) as $row) {
|
||||
if ($row->deleted_at !== null) {
|
||||
continue;
|
||||
}
|
||||
if ($kind !== 'locations') {
|
||||
$file = $row->file;
|
||||
$doc = DB::table('documents')->where('id', $row->document_id)->first();
|
||||
if (! $doc || $doc->deleted_at !== null || $doc->status !== 'active' || $doc->malware_scan_status !== 'clean'
|
||||
|| ! hash_equals(trim($doc->sha256), trim((string) $doc->malware_scanned_sha256))) {
|
||||
throw ValidationException::withMessages(['attachments' => 'Aguarde uma análise de segurança válida dos anexos.']);
|
||||
}
|
||||
$result[$kind][$row->id] = ['version' => $row->version, 'document_id' => $row->document_id, 'payload' => $row->payload, 'file' => $file];
|
||||
} else {
|
||||
$result[$kind][$row->id] = ['version' => $row->version, 'payload' => $row->payload];
|
||||
}
|
||||
}
|
||||
ksort($result[$kind]);
|
||||
}
|
||||
if (count($result['images']) > 6) {
|
||||
throw ValidationException::withMessages(['images' => 'A oferta admite no máximo seis fotografias.']);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Opportunity;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
|
||||
|
||||
/** Availability, reports and administrative plans in the existing opportunity and CRM tasks. */
|
||||
final class PrivateOfferOperations
|
||||
{
|
||||
/** @param array<string,mixed> $context
|
||||
* @return array<string,mixed> */
|
||||
public function afterApproval(array $context): array
|
||||
{
|
||||
$days = config('private_offers.validity_days');
|
||||
$reference = config('private_offers.validity_policy_reference');
|
||||
if ($days !== null && (! is_int($days) || $days < 1 || $days > 36500 || ! is_string($reference) || trim($reference) === '')) {
|
||||
throw ValidationException::withMessages(['validity' => 'Configuração de validade incompleta. Não foi aplicado um prazo presumido.']);
|
||||
}
|
||||
$operation = $context['offer_operation'] ?? ['status' => 'available', 'cycle' => 0];
|
||||
$hash = $context['approval']['hash'];
|
||||
$policy = ['days' => $days, 'policy_reference' => $reference, 'approval_hash' => $hash];
|
||||
$operation['validity_policies'][$hash] ??= $policy;
|
||||
$operation['validity'] ??= $operation['validity_policies'][$hash];
|
||||
$operation['last_approval_hash'] = $hash;
|
||||
|
||||
return $operation;
|
||||
}
|
||||
|
||||
private function offer(int $id, bool $lock = false): Opportunity
|
||||
{
|
||||
$record = Opportunity::query()->when($lock, fn ($q) => $q->lockForUpdate())->findOrFail($id);
|
||||
abort_unless($record->isPrivateOfferPreparation(), 404);
|
||||
|
||||
return $record;
|
||||
}
|
||||
|
||||
public function publisher(User $actor, Opportunity $record): bool
|
||||
{
|
||||
$access = app(PortalProfileAccess::class);
|
||||
|
||||
return $access->canView($actor, $record) && $access->mfa($actor) && ! $access->has($actor, 'auditor') && ! $access->conflict($actor, $record)
|
||||
&& $access->scoped($actor, 'publisher', $record->organization_id, $record->opportunity_category) && $access->operational($actor, 'publish', $record->id);
|
||||
}
|
||||
|
||||
/** @return Builder<Opportunity> */
|
||||
public function staffQuery(User $actor): Builder
|
||||
{
|
||||
$access = app(PortalProfileAccess::class);
|
||||
abort_unless($access->mfa($actor) && ! $access->has($actor, 'auditor'), 403);
|
||||
$grants = [...$access->grants($actor, 'validator'), ...$access->grants($actor, 'publisher')];
|
||||
abort_unless($grants !== [], 403);
|
||||
|
||||
return $access->scope(Opportunity::query(), $actor)->whereNotNull('workflow_context->private_offer_preparation')
|
||||
->where('owner_user_id', '!=', $actor->id)->whereRaw("NOT COALESCE(workflow_context->'contributors','[]'::jsonb) @> ?::jsonb", [json_encode([$actor->id])])
|
||||
->where(fn ($q) => $q->whereNull('organization_id')->orWhereNotIn('organization_id', DB::table('memberships')->where('user_id', $actor->id)->whereNull('deleted_at')->select('organization_id')))
|
||||
->where(function ($q) use ($grants) {
|
||||
foreach ($grants as $grant) {
|
||||
$q->orWhere(function ($scope) use ($grant) {
|
||||
$scope->whereIn('opportunity_category', json_decode($grant->opportunity_types, true, 512, JSON_THROW_ON_ERROR));
|
||||
if ($grant->organization_id !== null) {
|
||||
$scope->where('organization_id', $grant->organization_id);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
public function presentation(User $actor, int $id): array
|
||||
{
|
||||
$record = app(PrivateOfferPreparation::class)->record($actor, $id);
|
||||
$reviewer = app(PrivateOfferConfidentiality::class)->internal($actor, $record);
|
||||
$publisher = $this->publisher($actor, $record);
|
||||
$reports = [];
|
||||
if ($reviewer) {
|
||||
foreach ($record->workflow_context['offer_reports'] ?? [] as $report) {
|
||||
$reports[] = [...$report, 'reason' => Crypt::decryptString($report['reason']), 'resolution_reason' => isset($report['resolution_reason']) ? Crypt::decryptString($report['resolution_reason']) : null,
|
||||
'source_snapshot' => isset($report['source_snapshot']) ? json_decode(Crypt::decryptString($report['source_snapshot']), true, 512, JSON_THROW_ON_ERROR) : null];
|
||||
}
|
||||
}
|
||||
$highlight = $record->workflow_context['offer_highlight'] ?? null;
|
||||
|
||||
return ['id' => $id, 'etag' => app(OpportunityManagement::class)->etag($record),
|
||||
'availability' => app(OfferAvailability::class)->record($record), 'cycle' => $record->workflow_context['offer_operation']['cycle'] ?? 0,
|
||||
'owner' => app(PrivateOfferConfidentiality::class)->seller($actor, $record), 'reviewer' => $reviewer, 'publisher' => $publisher,
|
||||
'editorial_state' => app(PortalProfileAccess::class)->state($record), 'reports' => $reports,
|
||||
'highlight' => $publisher && $highlight ? [...$highlight, 'note' => Crypt::decryptString($highlight['note'])] : null,
|
||||
'commercial_activation' => false,
|
||||
'history' => array_map(fn ($event) => array_intersect_key($event, array_flip(['id', 'action', 'actor_id', 'at', 'cycle'])), $record->workflow_context['offer_operation_history'] ?? [])];
|
||||
}
|
||||
|
||||
/** @param array<string,mixed> $context
|
||||
* @param array<string,mixed> $metadata */
|
||||
private function save(Opportunity $record, ?User $actor, array $context, string $action, array $metadata = []): void
|
||||
{
|
||||
$event = ['id' => (string) Str::uuid(), 'action' => $action, 'actor_id' => $actor?->id, 'at' => now()->toIso8601String(),
|
||||
'cycle' => $context['offer_operation']['cycle'] ?? 0, ...$metadata];
|
||||
$context['offer_operation_history'][] = $event;
|
||||
$record->workflow_context = $context;
|
||||
$record->management_version++;
|
||||
$record->save();
|
||||
DB::table('audit_events')->insert(['source_system' => 'private-offers', 'entity_type' => 'opportunity', 'actor_user_id' => $actor?->id,
|
||||
'action' => $action, 'data_classification' => 'restricted', 'correlation_id' => (string) Str::uuid(),
|
||||
'after_data' => json_encode(['record_id' => $record->id, 'record_version' => $record->management_version, ...$event], JSON_THROW_ON_ERROR)]);
|
||||
}
|
||||
|
||||
/** @param array<string,mixed> $context
|
||||
* @return array<string,mixed> */
|
||||
private function withdraw(array $context, string $origin): array
|
||||
{
|
||||
$context['offer_operation'] = [...($context['offer_operation'] ?? ['cycle' => 0]), 'status' => 'withdrawn', 'withdrawn_at' => now()->toIso8601String(), 'origin' => $origin];
|
||||
|
||||
return $context;
|
||||
}
|
||||
|
||||
/** @param array<string,mixed> $input */
|
||||
public function ownerAction(User $actor, int $id, array $input): Opportunity
|
||||
{
|
||||
$data = validator($input, ['action' => 'required|in:withdraw,request_reactivation,reactivate', 'confirm' => 'required|accepted',
|
||||
'etag' => 'required_unless:action,withdraw|string|size:64', 'reason' => 'nullable|string|max:1000'])->validate();
|
||||
|
||||
return DB::transaction(function () use ($actor, $id, $data) {
|
||||
$record = $this->offer($id, true);
|
||||
abort_unless(app(PrivateOfferConfidentiality::class)->seller($actor, $record), 403);
|
||||
$context = $record->workflow_context;
|
||||
$state = app(OfferAvailability::class)->record($record)['state'];
|
||||
if ($data['action'] === 'withdraw') {
|
||||
if (($context['offer_operation']['status'] ?? null) === 'withdrawn') {
|
||||
return $record;
|
||||
}
|
||||
$this->save($record, $actor, $this->withdraw($context, 'owner'), 'offer_withdrawn', ['reason' => Crypt::encryptString($data['reason'] ?? '')]);
|
||||
|
||||
return $record;
|
||||
}
|
||||
abort_unless(hash_equals(app(OpportunityManagement::class)->etag($record), $data['etag']), 409);
|
||||
if ($data['action'] === 'request_reactivation') {
|
||||
abort_unless(in_array($state, ['withdrawn', 'expired'], true), 409, 'A oferta não está retirada ou expirada.');
|
||||
$editorialState = app(PortalProfileAccess::class)->state($record);
|
||||
abort_unless(isset($context['submission']) || in_array($editorialState, ['draft', 'needs_information'], true), 409,
|
||||
'A preparação anterior ainda precisa da vinculação editorial existente antes de abrir nova revisão.');
|
||||
$context['offer_operation'] = [...($context['offer_operation'] ?? []), 'status' => 'revalidation', 'cycle' => ($context['offer_operation']['cycle'] ?? 0) + 1,
|
||||
'requested_by' => $actor->id, 'requested_at' => now()->toIso8601String()];
|
||||
// Reopen the same editorial engine, retaining old submissions and decisions.
|
||||
$record->workflow_context = $context;
|
||||
$record->save();
|
||||
if (isset($context['submission'])) {
|
||||
$record = app(OpportunityProfileWorkflow::class)->transition($actor, $id, ['etag' => app(OpportunityManagement::class)->etag($record),
|
||||
'action' => 'request_revision', 'reason' => 'Pedido explícito de nova validação para reactivar a oferta. '.($data['reason'] ?? '')]);
|
||||
}
|
||||
$this->save($record, $actor, $record->workflow_context, 'offer_reactivation_requested');
|
||||
|
||||
return $record;
|
||||
}
|
||||
abort_unless($state === 'revalidation' && app(PortalProfileAccess::class)->state($record) === 'approved', 409, 'É necessária nova validação editorial.');
|
||||
$snapshot = app(PrivateOfferPreparation::class)->snapshot($record);
|
||||
abort_unless(($snapshot['payload']['operational_cycle'] ?? null) === $context['offer_operation']['cycle']
|
||||
&& hash_equals($context['approval']['hash'] ?? '', $snapshot['hash'])
|
||||
&& ($context['submission']['payload']['operational_cycle'] ?? null) === $context['offer_operation']['cycle'], 409);
|
||||
$context['offer_operation']['status'] = 'available';
|
||||
$context['offer_operation']['activated_hash'] = $snapshot['hash'];
|
||||
$this->save($record, $actor, $context, 'offer_reactivated_privately');
|
||||
|
||||
return $record;
|
||||
});
|
||||
}
|
||||
|
||||
public function expire(int $id): bool
|
||||
{
|
||||
return DB::transaction(function () use ($id) {
|
||||
$record = $this->offer($id, true);
|
||||
$availability = app(OfferAvailability::class)->record($record);
|
||||
if ($availability['state'] !== 'expired' || ($record->workflow_context['offer_operation']['status'] ?? null) === 'expired') {
|
||||
return false;
|
||||
}
|
||||
$context = $record->workflow_context;
|
||||
$context['offer_operation'] = [...($context['offer_operation'] ?? []), 'status' => 'expired', 'expired_at' => $availability['expires_at']];
|
||||
$this->save($record, null, $context, 'offer_expired', ['expires_at' => $availability['expires_at']]);
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
public function canReport(User $actor, Opportunity $record): bool
|
||||
{
|
||||
if (app(PortalProfileAccess::class)->canView($actor, $record)) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
app(OfferPublicCatalogue::class)->detail($record->id);
|
||||
|
||||
return true;
|
||||
} catch (HttpExceptionInterface $error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** @param array<string,mixed> $input
|
||||
* @return array<string,mixed> */
|
||||
public function report(User $actor, int $id, array $input): array
|
||||
{
|
||||
$data = validator($input, ['client_key' => 'required|uuid', 'reason' => 'required|string|min:20|max:2000', 'confirm' => 'required|accepted'])->validate();
|
||||
|
||||
return DB::transaction(function () use ($actor, $id, $data) {
|
||||
$record = $this->offer($id, true);
|
||||
$context = $record->workflow_context;
|
||||
$key = $data['client_key'];
|
||||
$hash = hash('sha256', $data['reason']);
|
||||
if (isset($context['offer_reports'][$key])) {
|
||||
$previous = $context['offer_reports'][$key];
|
||||
abort_unless($previous['reporter_id'] === $actor->id, 404);
|
||||
abort_unless($previous['command_hash'] === $hash, 409);
|
||||
|
||||
return ['id' => $key, 'replayed' => true];
|
||||
}
|
||||
abort_unless($this->canReport($actor, $record), 404);
|
||||
try {
|
||||
$source = app(OfferPublicCatalogue::class)->detail($id);
|
||||
} catch (HttpExceptionInterface $error) {
|
||||
$source = Arr::only(app(OpportunityManagement::class)->values($record), ['title', 'description', 'opportunity_category', 'private_offer']);
|
||||
}
|
||||
$task = app(OpportunityWorkflowTasks::class)->ensure($record, 'offer_report_'.$key, 'Denúncia da Oferta '.$record->reference);
|
||||
$context['offer_reports'][$key] = ['id' => $key, 'reporter_id' => $actor->id, 'at' => now()->toIso8601String(), 'command_hash' => $hash,
|
||||
'reason' => Crypt::encryptString($data['reason']), 'version' => 1, 'status' => 'open', 'task_id' => $task,
|
||||
'source_snapshot' => Crypt::encryptString(json_encode($source, JSON_THROW_ON_ERROR)),
|
||||
'source_record_version' => $record->management_version, 'source_approval_hash' => $context['approval']['hash'] ?? null,
|
||||
'source_public_hash' => $context['offer_publication']['publication']['hash'] ?? null];
|
||||
$this->save($record, $actor, $context, 'offer_reported', ['report_id' => $key]);
|
||||
|
||||
return ['id' => $key, 'replayed' => false];
|
||||
});
|
||||
}
|
||||
|
||||
/** @param array<string,mixed> $input */
|
||||
public function handleReport(User $actor, int $id, string $reportId, array $input): void
|
||||
{
|
||||
$data = validator($input, ['etag' => 'required|string|size:64', 'version' => 'required|integer|min:1', 'action' => 'required|in:start_review,resolve_no_withdrawal,resolve_withdrawal',
|
||||
'reason' => 'required|string|min:20|max:2000', 'confirm' => 'required|accepted'])->validate();
|
||||
DB::transaction(function () use ($actor, $id, $reportId, $data) {
|
||||
$record = $this->offer($id, true);
|
||||
abort_unless(app(PrivateOfferConfidentiality::class)->internal($actor, $record), 403);
|
||||
abort_unless(hash_equals(app(OpportunityManagement::class)->etag($record), $data['etag']), 409);
|
||||
$context = $record->workflow_context;
|
||||
$report = $context['offer_reports'][$reportId] ?? null;
|
||||
abort_unless($report !== null, 404);
|
||||
abort_unless($report['version'] === (int) $data['version'] && in_array($report['status'], ['open', 'in_review'], true), 409);
|
||||
if ($data['action'] === 'start_review') {
|
||||
abort_unless($report['status'] === 'open', 409);
|
||||
app(OpportunityWorkflowTasks::class)->assign($record, $report['task_id'], $actor);
|
||||
$report['status'] = 'in_review';
|
||||
} else {
|
||||
abort_unless($report['status'] === 'in_review' && $report['reviewer_id'] === $actor->id, 409, 'A denúncia deve ser tratada pelo revisor que a iniciou.');
|
||||
app(OpportunityWorkflowTasks::class)->complete($report['task_id'], 'Denúncia tratada; decisão no histórico privado.');
|
||||
$report['status'] = 'resolved';
|
||||
$report['resolution'] = $data['action'];
|
||||
if ($data['action'] === 'resolve_withdrawal') {
|
||||
$context = $this->withdraw($context, 'moderation');
|
||||
}
|
||||
}
|
||||
$report['reviewer_id'] = $actor->id;
|
||||
$report['version']++;
|
||||
$report['updated_at'] = now()->toIso8601String();
|
||||
$report['resolution_reason'] = Crypt::encryptString($data['reason']);
|
||||
$report['history'][] = ['action' => $data['action'], 'actor_id' => $actor->id, 'at' => now()->toIso8601String(), 'reason' => Crypt::encryptString($data['reason'])];
|
||||
$context['offer_reports'][$reportId] = $report;
|
||||
$this->save($record, $actor, $context, 'offer_report_'.$data['action'], ['report_id' => $reportId]);
|
||||
});
|
||||
}
|
||||
|
||||
/** @param array<string,mixed> $input */
|
||||
public function highlight(User $actor, int $id, array $input): void
|
||||
{
|
||||
$data = validator($input, ['etag' => 'required|string|size:64', 'version' => 'required|integer|min:0', 'status' => 'required|in:draft,archived',
|
||||
'label' => 'required|string|max:150', 'starts_at' => 'nullable|date', 'ends_at' => 'nullable|required_with:starts_at|date|after:starts_at',
|
||||
'note' => 'required|string|min:20|max:1000', 'confirm' => 'required|accepted'])->validate();
|
||||
DB::transaction(function () use ($actor, $id, $data) {
|
||||
$record = $this->offer($id, true);
|
||||
abort_unless($this->publisher($actor, $record), 403);
|
||||
$context = $record->workflow_context;
|
||||
$before = $context['offer_highlight'] ?? null;
|
||||
abort_unless(hash_equals(app(OpportunityManagement::class)->etag($record), $data['etag']) && (int) $data['version'] === ($before['version'] ?? 0), 409);
|
||||
$plan = ['version' => (int) $data['version'] + 1, 'status' => $data['status'], 'label' => $data['label'], 'starts_at' => $data['starts_at'] ?? null,
|
||||
'ends_at' => $data['ends_at'] ?? null, 'note' => Crypt::encryptString($data['note']), 'actor_id' => $actor->id, 'at' => now()->toIso8601String()];
|
||||
$context['offer_highlight_history'][] = $before;
|
||||
$context['offer_highlight'] = $plan;
|
||||
$this->save($record, $actor, $context, 'offer_highlight_prepared', ['plan_version' => $plan['version'], 'status' => $plan['status']]);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Opportunity;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
/** Private commercial adapter: the existing opportunity owns identity, workflow and audit. */
|
||||
final class PrivateOfferPreparation
|
||||
{
|
||||
/** @return array<string,mixed> */
|
||||
public function validate(mixed $input, bool $complete = false): array
|
||||
{
|
||||
$validated = Validator::make(['private_offer' => $input], [
|
||||
'private_offer' => 'present|array:price_mode,amount,currency,contact_id,business,property',
|
||||
'private_offer.price_mode' => 'nullable|in:amount,negotiable',
|
||||
'private_offer.amount' => ['nullable', 'string', ...($complete ? ['required_if:private_offer.price_mode,amount'] : []), 'prohibited_unless:private_offer.price_mode,amount', 'regex:/^\d{1,16}(\.\d{1,2})?$/'],
|
||||
'private_offer.currency' => ['nullable', ...($complete ? ['required_if:private_offer.price_mode,amount'] : []), 'prohibited_unless:private_offer.price_mode,amount', 'in:MZN,USD'],
|
||||
'private_offer.contact_id' => 'nullable|uuid',
|
||||
'private_offer.business' => 'sometimes|nullable|array',
|
||||
'private_offer.property' => 'sometimes|nullable|array',
|
||||
])->validate()['private_offer'];
|
||||
|
||||
return array_replace($validated, app(PrivateOfferDetails::class)->validate(array_intersect_key($validated, array_flip(['business', 'property']))));
|
||||
}
|
||||
|
||||
/** @param array<string,mixed> $input */
|
||||
public function save(User $actor, array $input, ?int $id = null): Opportunity
|
||||
{
|
||||
foreach (['title', 'description'] as $field) {
|
||||
if (is_string($input[$field] ?? null)) {
|
||||
$input[$field] = trim($input[$field]);
|
||||
}
|
||||
}
|
||||
$allowed = ['creation_key', 'etag', 'reason', 'title', 'description', 'opportunity_category', 'organization_id', 'private_offer', ...array_keys(InvestmentGeography::FIELDS)];
|
||||
if (array_diff(array_keys($input), $allowed)) {
|
||||
throw ValidationException::withMessages(['offer' => 'O pedido contém campos fora da preparação privada.']);
|
||||
}
|
||||
Validator::make($input, ['opportunity_category' => ['sometimes', 'required', Rule::in(OpportunityManagement::CATEGORIES)]])->validate();
|
||||
|
||||
return DB::transaction(function () use ($actor, $input, $id) {
|
||||
$record = app(OpportunityManagement::class)->save($actor, $input, $id, true);
|
||||
$values = app(OpportunityManagement::class)->values($record);
|
||||
app(PrivateOfferDetails::class)->assertCategory($values['private_offer'] ?? [], $values['opportunity_category']);
|
||||
$contactId = app(OpportunityManagement::class)->values($record)['private_offer']['contact_id'] ?? null;
|
||||
if ($contactId && ! app(OpportunityContacts::class)->related($record, $contactId)) {
|
||||
throw ValidationException::withMessages(['private_offer.contact_id' => 'Seleccione um contacto autorizado do promotor.']);
|
||||
}
|
||||
|
||||
return $record;
|
||||
});
|
||||
}
|
||||
|
||||
public function record(User $actor, int $id, bool $edit = false): Opportunity
|
||||
{
|
||||
$record = Opportunity::findOrFail($id);
|
||||
abort_unless($record->isPrivateOfferPreparation(), 404);
|
||||
app(PortalProfileAccess::class)->authorize($actor, $edit ? 'edit' : 'view', $record);
|
||||
|
||||
return $record;
|
||||
}
|
||||
|
||||
/** Register submission through the existing editorial engine when available.
|
||||
* @param array<string,mixed> $input */
|
||||
public function submit(User $actor, int $id, array $input): Opportunity
|
||||
{
|
||||
$data = Validator::make($input, ['etag' => 'required|string|size:64', 'confirm' => 'required|accepted'])->validate();
|
||||
if (app(PortalEditorialWorkflow::class)->available()) {
|
||||
$record = $this->record($actor, $id);
|
||||
$access = app(PortalProfileAccess::class);
|
||||
if ($access->state($record) === 'submitted') {
|
||||
abort_unless($access->promoterRecordScope($actor, $record) && $access->canCreate($actor, $record->organization_id, $record->opportunity_category)
|
||||
&& $access->operational($actor, 'edit', $id), 403);
|
||||
abort_unless(hash_equals(app(OpportunityManagement::class)->etag($record), $data['etag']), 409);
|
||||
|
||||
return $record;
|
||||
}
|
||||
|
||||
return app(OpportunityProfileWorkflow::class)->transition($actor, $id, [...$data, 'action' => 'submit', 'reason' => 'Submissão privada da oferta conferida pelo promotor.']);
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($actor, $id, $data) {
|
||||
$record = Opportunity::whereKey($id)->lockForUpdate()->firstOrFail();
|
||||
abort_unless($record->isPrivateOfferPreparation(), 404);
|
||||
$access = app(PortalProfileAccess::class);
|
||||
$access->authorize($actor, 'view', $record);
|
||||
abort_if(($record->workflow_context['private_offer_preparation']['version'] ?? 1) >= 2, 503, 'O circuito editorial está indisponível. A versão e o histórico foram preservados; não é permitida a retoma pelo percurso anterior.');
|
||||
abort_unless($access->promoterRecordScope($actor, $record)
|
||||
&& $access->canCreate($actor, $record->organization_id, $record->opportunity_category)
|
||||
&& $access->operational($actor, 'edit', $record->id), 403);
|
||||
$management = app(OpportunityManagement::class);
|
||||
abort_unless(hash_equals($management->etag($record), $data['etag']), 409, 'A oferta mudou. Recupere a versão actual antes de submeter.');
|
||||
if (data_get($record->workflow_context, 'private_offer_preparation.state') === 'submitted') {
|
||||
return $record;
|
||||
}
|
||||
$access->authorize($actor, 'edit', $record);
|
||||
$values = $management->values($record);
|
||||
Validator::make($values, ['title' => 'required|string|max:150', 'description' => 'required|string|max:50000',
|
||||
'opportunity_category' => 'required', 'private_offer.price_mode' => 'required|in:amount,negotiable', 'private_offer.contact_id' => 'required|uuid'])->validate();
|
||||
$this->validate($values['private_offer'], true);
|
||||
if (! app(OpportunityContacts::class)->related($record, $values['private_offer']['contact_id'])) {
|
||||
throw ValidationException::withMessages(['private_offer.contact_id' => 'O contacto já não está disponível neste âmbito.']);
|
||||
}
|
||||
// Recheck revoked grants and representation inside the same transaction.
|
||||
abort_unless($access->canCreate($actor, $record->organization_id, $record->opportunity_category), 403);
|
||||
$path = app(InvestmentGeography::class)->path($record->primary_territory_id, true);
|
||||
if ($record->primary_territory_id && $path === []) {
|
||||
throw ValidationException::withMessages(['province_id' => 'Reconcilie a localização antes de submeter.']);
|
||||
}
|
||||
if ($path !== []) {
|
||||
app(InvestmentGeography::class)->selection(array_combine(array_slice(array_keys(InvestmentGeography::FIELDS), 0, count($path)), array_column($path, 'id')), true);
|
||||
}
|
||||
$context = $record->workflow_context;
|
||||
$context['private_offer_preparation'] = ['version' => 1, 'state' => 'submitted', 'submitted_by' => $actor->id,
|
||||
'submitted_at' => now()->toIso8601String(), 'submitted_revision' => $record->management_version];
|
||||
$record->workflow_context = $context;
|
||||
$record->management_version++;
|
||||
$record->save();
|
||||
$access->audit($actor, 'private_offer_submitted', ['record_id' => $record->id, 'version' => $record->management_version, 'public' => false]);
|
||||
|
||||
return $record;
|
||||
});
|
||||
}
|
||||
|
||||
/** Adapter of the five common fields; never relaxes the full canonical contract.
|
||||
* @return array<string,mixed> */
|
||||
public function snapshot(Opportunity $record): array
|
||||
{
|
||||
$values = app(OpportunityManagement::class)->values($record);
|
||||
Validator::make($values, ['title' => 'required|string|max:150', 'description' => 'required|string|max:50000',
|
||||
'opportunity_category' => 'required', 'private_offer.price_mode' => 'required|in:amount,negotiable', 'private_offer.contact_id' => 'required|uuid'])->validate();
|
||||
$offer = $this->validate($values['private_offer'], true);
|
||||
app(PrivateOfferDetails::class)->assertCategory($offer, $values['opportunity_category']);
|
||||
$resources = app(PrivateOfferDetails::class)->resources($record);
|
||||
if (! app(OpportunityContacts::class)->related($record, $offer['contact_id'])) {
|
||||
throw ValidationException::withMessages(['private_offer.contact_id' => 'O contacto já não está disponível neste âmbito.']);
|
||||
}
|
||||
$contact = DB::table('contacts')->where('id', $offer['contact_id'])->first();
|
||||
$path = app(InvestmentGeography::class)->path($values['primary_territory_id'] ?? null, true);
|
||||
if (! empty($values['primary_territory_id']) && $path === []) {
|
||||
throw ValidationException::withMessages(['province_id' => 'Reconcilie a localização antes da decisão.']);
|
||||
}
|
||||
if ($path !== []) {
|
||||
app(InvestmentGeography::class)->selection(array_combine(array_slice(array_keys(InvestmentGeography::FIELDS), 0, count($path)), array_column($path, 'id')), true);
|
||||
}
|
||||
$payload = ['title' => $values['title'], 'description' => $values['description'], 'opportunity_category' => $values['opportunity_category'],
|
||||
'owner_user_id' => $record->owner_user_id, 'organization_id' => $record->organization_id, 'private_offer' => $offer,
|
||||
'contact_fingerprint' => hash('sha256', json_encode($contact, JSON_THROW_ON_ERROR)), 'territory' => $path];
|
||||
$extended = isset($offer['business']) || isset($offer['property']) || array_filter($resources) !== [];
|
||||
if ($extended) {
|
||||
$payload['resources'] = $resources;
|
||||
}
|
||||
$cycle = $record->workflow_context['offer_operation']['cycle'] ?? 0;
|
||||
if ($cycle > 0) {
|
||||
$payload['operational_cycle'] = $cycle;
|
||||
}
|
||||
$normalize = function (mixed $value) use (&$normalize): mixed {
|
||||
if (is_object($value)) {
|
||||
$value = get_object_vars($value);
|
||||
}
|
||||
if (! is_array($value)) {
|
||||
return $value;
|
||||
}
|
||||
if (! array_is_list($value)) {
|
||||
ksort($value);
|
||||
}
|
||||
|
||||
return array_map($normalize, $value);
|
||||
};
|
||||
$payload = $normalize($payload);
|
||||
|
||||
$contract = $cycle > 0 ? 'private-offer-editorial-v4' : ($extended ? 'private-offer-editorial-v3' : 'private-offer-editorial-v2');
|
||||
|
||||
return ['payload' => $payload, 'hash' => hash('sha256', json_encode(['contract' => $contract, 'payload' => $payload], JSON_THROW_ON_ERROR)),
|
||||
'record_version' => $record->management_version, 'contract_version' => $contract, 'resources' => $resources];
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
public function presentation(User $actor, Opportunity $record): array
|
||||
{
|
||||
app(PortalProfileAccess::class)->authorize($actor, 'view', $record);
|
||||
$values = app(OpportunityManagement::class)->values($record);
|
||||
$fresh = null;
|
||||
$contactId = $values['private_offer']['contact_id'] ?? null;
|
||||
$contactLabel = $contactId && app(OpportunityContacts::class)->related($record, $contactId)
|
||||
? DB::table('contacts')->where('id', $contactId)->value('full_name') : null;
|
||||
if (isset($record->workflow_context['approval'])) {
|
||||
try {
|
||||
$fresh = hash_equals($record->workflow_context['approval']['hash'], $this->snapshot($record)['hash']);
|
||||
} catch (ValidationException $error) {
|
||||
$fresh = false;
|
||||
}
|
||||
}
|
||||
$history = $record->workflow_context['events'] ?? [];
|
||||
$names = User::whereIn('id', array_filter([...array_column($history, 'actor_id'), ...array_column($history, 'assignee_user_id')]))->pluck('name', 'id');
|
||||
$history = array_map(fn ($event) => [...$event, 'actor_name' => $names[$event['actor_id']] ?? 'Interveniente indisponível',
|
||||
'assignee_name' => isset($event['assignee_user_id']) ? ($names[$event['assignee_user_id']] ?? 'Interveniente indisponível') : null], $history);
|
||||
$versions = [...($record->workflow_context['decision_history'] ?? []), ...($record->workflow_context['submission_history'] ?? [])];
|
||||
if (isset($record->workflow_context['submission'])) {
|
||||
$versions[] = ['submission' => $record->workflow_context['submission'], 'approval' => $record->workflow_context['approval'] ?? null,
|
||||
'rejection' => $record->workflow_context['rejection'] ?? null];
|
||||
}
|
||||
$uniqueVersions = [];
|
||||
foreach ($versions as $version) {
|
||||
if (! isset($version['submission']['payload']['private_offer'])) {
|
||||
continue;
|
||||
}
|
||||
$key = $version['submission']['record_version'].':'.$version['submission']['hash'].':'.($version['submission']['review_cycle'] ?? 0);
|
||||
$previous = $uniqueVersions[$key] ?? [];
|
||||
$uniqueVersions[$key] = ['submission' => $version['submission'], 'approval' => $version['approval'] ?? $previous['approval'] ?? null,
|
||||
'rejection' => $version['rejection'] ?? $previous['rejection'] ?? null];
|
||||
}
|
||||
$workflow = app(OpportunityProfileWorkflow::class);
|
||||
$actions = $workflow->actions($actor, $record);
|
||||
|
||||
return ['id' => $record->id, 'reference' => $record->reference, 'values' => $values,
|
||||
'etag' => app(OpportunityManagement::class)->etag($record), 'state' => app(PortalProfileAccess::class)->state($record),
|
||||
'canEdit' => app(PortalProfileAccess::class)->canEdit($actor, $record), 'organization_id' => $record->organization_id,
|
||||
'actions' => $actions,
|
||||
'reviewers' => in_array('assign_review', $actions, true) ? $workflow->participants($actor, $record)['validators'] : [],
|
||||
'reauthenticationRequired' => app(OpportunityProfileWorkflow::class)->reauthenticationRequired($actor, $record),
|
||||
'history' => $history, 'submittedVersions' => array_values($uniqueVersions), 'correctionItems' => $record->workflow_context['correction_items'] ?? [],
|
||||
'approval' => $record->workflow_context['approval'] ?? null, 'approvalFresh' => $fresh, 'contactLabel' => $contactLabel, 'tasks' => app(OpportunityWorkflowTasks::class)->listing($record),
|
||||
'territory' => app(InvestmentGeography::class)->path($values['primary_territory_id'] ?? null, true)];
|
||||
}
|
||||
}
|
||||
@@ -61,7 +61,11 @@ class VerifiedDocumentBinary
|
||||
if ($table === 'opportunity_images') {
|
||||
$query->orWhereRaw("payload->'derivatives' @> ?::jsonb", [json_encode([['document_id' => $document->id]], JSON_THROW_ON_ERROR)]);
|
||||
}
|
||||
$links = $links->concat($query->get(['opportunity_id', 'deleted_at', 'payload']));
|
||||
$rows = $query->get(['opportunity_id', 'deleted_at', 'payload']);
|
||||
foreach ($rows as $link) {
|
||||
$link->resource_kind = $table;
|
||||
}
|
||||
$links = $links->concat($rows);
|
||||
}
|
||||
$privateOrigin = in_array($document->source_system ?? null, ['opportunity-resources', 'opportunity-image-derivatives'], true);
|
||||
if (! $privateOrigin) {
|
||||
@@ -72,6 +76,15 @@ class VerifiedDocumentBinary
|
||||
}
|
||||
$actor = request()->user();
|
||||
abort_unless($actor instanceof User, 403, 'Anexo privado: é necessário acesso autorizado à oportunidade.');
|
||||
foreach ($links as $link) {
|
||||
$offer = Opportunity::withTrashed()->whereKey($link->opportunity_id)->first();
|
||||
if ($offer?->isPrivateOfferPreparation() && $link->resource_kind === 'opportunity_documents') {
|
||||
$allowed = $link->deleted_at === null && ! $offer->trashed() && app(PrivateOfferConfidentiality::class)->internal($actor, $offer);
|
||||
app(PortalProfileAccess::class)->audit($actor, $allowed ? 'offer_proof_access_authorized' : 'offer_proof_access_denied',
|
||||
['record_id' => $offer->id, 'document_id' => $document->id, 'record_version' => $offer->management_version, 'sha256' => trim($document->sha256)]);
|
||||
abort_unless($allowed, 403, 'Comprovativo reservado à validação interna autorizada.');
|
||||
}
|
||||
}
|
||||
foreach ($links as $link) {
|
||||
$opportunity = Opportunity::whereKey($link->opportunity_id)->first();
|
||||
if ($link->deleted_at === null && $opportunity && $opportunity->status !== 'archived'
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
// No legal text or operational NDA is supplied/approved. Never infer it from an upload.
|
||||
'nda' => null,
|
||||
// No definitive publication lifetime was decided. Freeze an explicit policy on approval.
|
||||
'validity_days' => null,
|
||||
'validity_policy_reference' => null,
|
||||
'expiry_scheduler_enabled' => false,
|
||||
];
|
||||
@@ -10139,3 +10139,84 @@ Mudanças de datas/fontes não substituem conclusões ou valores. A correspondê
|
||||
QA:70testes frontend afectados distintos, incluindo nove novos de domínio/UI; sucesso, falta de fonte, divergência de titular/datas, período anterior curto/não contíguo, reconferência independente, preservação da distribuição, mês único e histórico. API nativa POST/PUT/GET:quatro cenários sintéticos,46asserções, rollback em investmoz_demography_test:55441, identidade PDO conferida. Cálculo→gravação→recuperação→relatório financeiro do mesmo dossier demonstrados. Tipos, lint focado, Pint e compilação aprovados. Edge com componentes reais compilados:112000MZN nos meses relativos correctos, concentração emJaneiro2026, total indeterminado sem fonte, PEC preservado quando PPC muda e193000MZN apenas após reconferência. Corrigido aviso contraditório de ano civil no caso documental incompleto; regressão acrescentada. Provas:outputs/irpc-noncivil-20261114/. Provas aceites não afectadas reutilizadas.
|
||||
|
||||
Estado:implementado e verificado em QA, ainda sem instalação. Homologação mantém078f770d. Este lote encerra apenas as estimativas PPC/PEC e a projecção deste recorte anual não civil documentado; não encerra L6/L8, a liquidação final não civil, o simulador ou o portal. Resíduos L1–L7/restanteL8/L9–L16 e outros módulos preservados na matriz. Sem migrações, permissões, rotas, importações, pagamentos, declarações submetidas ou novas deduções. Instalação agrupada depende de autorização concreta, executor Git, backup/restauro fresco e reversão de release preservando edições posteriores.
|
||||
|
||||
## 14/11/2026 — Ofertas, Fase 1: preparação privada em QA
|
||||
|
||||
Autorização condicionada expressa da Tempo nesta sessão: exclusivamente desenvolvimento da Fase 1. Oportunidades é o cadastro principal das novas Ofertas; negócios e restantes percursos mantêm os seus identificadores, contratos e dados. Sem mudanças no menu, esquema mne, dados operacionais ou instalação. As migrações existentes foram usadas apenas para preparar uma base sintética nova, offers_phase1_20261114_test:55441, preservando as outras bases QA.
|
||||
|
||||
Entrada privada em desenvolvimento: /minhas-oportunidades/ofertas. Requer autenticação, email verificado, perfis disponíveis e âmbito de Promotor já atribuído para criar/editar/submeter; não concede papéis. Criar → guardar → recuperar → editar → conferir → submeter em privado. Rascunhos incompletos são recuperáveis. A submissão exige título, descrição, categoria, contacto autorizado e preço MZN/USD ou «A negociar»; não converte moedas nem confunde preço pedido com investimento estimado. Localização usa os identificadores e a hierarquia existentes. Contactos usam o CRM e o seu controlo de MFA, sem novo cadastro ou publicação dos contactos.
|
||||
|
||||
Gravação usa OpportunityManagement, chave de criação, etag, versões, management_draft e auditoria existentes. Campos inesperados são recusados; permissões, representação e contacto são reconferidos no servidor. Conflitos não sobrescrevem a versão guardada. A submissão regista autor, instante UTC e revisão nos metadados privados, sem criar revisão editorial, notificações, aprovação ou publicação da Fase 2. A oferta submetida fica recuperável e sem edição nesta fase. A marca privada bloqueia conversão/publicação pelo modelo, workflow e divulgação existentes; o catálogo público exclui-a mesmo perante um estado incoerente introduzido por outro escritor.
|
||||
|
||||
Verificação: 14 testes novos. Bateria de regressão de Oportunidades/perfis/editorial: 35 testes e 548 asserções aprovados. Passagem final focada, após as últimas alterações, com 16 testes e 172 asserções aprovados; os grupos sobrepõem-se e não devem ser somados. Inclui criação idempotente, recuperação, conflito, contacto de outro titular/eliminado, perda de representação/permissão, preço incompleto/inválido, campos forjados, submissão confirmada, bloqueios de publicação e ausência nas consultas públicas. Duas páginas verificadas por HTML completo com entradas Vite reais. Pint, PHPStan, tipos, lint/formatação focados, build QA e revisão do delta aprovados. Recibo: outputs/offers-private-phase1-20261114/qa-result.json.
|
||||
|
||||
Limitações: conferência visual manual não efectuada. npm run check global acusa formatação preexistente em 61 ficheiros alheios ao delta; esses ficheiros não foram alterados e a falha não foi dispensada. A bateria canónica extensa foi interrompida, não contabilizada como aprovada e substituída pela verificação do cenário afectado. Uma prova antiga de divulgação exige outra base pelo seu próprio controlo de segurança; esse controlo foi preservado, sem tocar nessa base, e a compatibilidade do âmbito nativo foi testada na nova preparação sintética.
|
||||
|
||||
Saldo de RF-MN-003: Fase 1 de Ofertas implementada e verificada tecnicamente em QA, sem instalação. Não encerra o requisito integral nem Procuras. Fase 2 e seguintes permanecem por autorizar; NDA, comprovativos, fotografias, validade, moderação e publicação não são simulados nesta entrega. A autorização anterior de validador jurídico para legislação não é estendida a Ofertas. Instalação em homologação ou produção exige verificação e autorização expressa separada.
|
||||
## 14/11/2026 — Ofertas Fase 2, circuito editorial privado em QA
|
||||
|
||||
Autorização expressa da Tempo, após encerramento técnico da F1 com ressalva visual. Implementação limitada ao plano 6e302db6, mantendo Oportunidades como cadastro principal. Dashboard existente → Ofertas/revisão privada → validador inicia ou coordenação atribui → pede correcção/rejeita com fundamento → promotor corrige/reenvia no mesmo identificador → aprovação interna independente. Preserva versões, rejeições, aprovação histórica e tarefas. Reatribuição guarda quem alterou, anterior e novo responsável, sem conceder papéis.
|
||||
|
||||
Snapshots/hash, revisão e ciclo vinculam cada decisão aos campos e referências conferidos. Mudança no contacto/localização bloqueia decisão ou assinala reconferência; não substitui a conclusão. Primeira revisão explícita de submissão F1 preserva autor/data/revisão e regista a vinculação actual sem inventar fingerprints históricos do CRM. Estados editoriais privados não tornam a oferta pública; status de publicação mantém-se privado, marca/exclusão pública e percursos alternativos bloqueados. Os percursos nativos conservam a versão pública anterior durante alterações pendentes. Desactivar o circuito não permite submeter v2 pelo fallback v1 nem apagar histórico.
|
||||
|
||||
Reutiliza perfis/MFA/independência, workflow, tarefas, auditoria e caixa de notificações. Email pelo transporte existente, depois do commit e com revalidação, activação/lista operacional nativas; em QA apenas memória, sem mensagens reais ou SMS. Configurações de entrega e designações operacionais não são inventadas. Sem migrações/tabelas novas, mudanças em mne, dados operacionais, menu principal ou instalação.
|
||||
|
||||
QA sintética offers_phase1_20261114_test:55441: regressão 35/499, final F2 11/160, complemento de contribuidores 1/14 e HTML final 1/10 aprovados; grupos sobrepostos, não somar. Pint, PHPStan, tipos, lint/formatação focados e build QA aprovados. Testa autoria/contribuidores/âmbito/MFA/revogação, snapshots, rejeição/correcção/reenvio, reatribuição, fila sem duplicação, rollback, notificações idempotentes, fonte alterada, recuperação e recusa de publicação. Conferência visual mantém a ressalva da ferramenta; formatação global continua nos 61 ficheiros preexistentes exteriores ao delta, sem dispensa. Entregabilidade real não demonstrada. Relatório: docs/investmoz/v2/auditoria-portal/ofertas-fase2-entrega-qa-20261114.md. Recibo: outputs/offers-private-phase2-20261114/qa-result.json.
|
||||
|
||||
Saldo RF-MN-003: F1/F2 implementadas e verificadas tecnicamente em QA, não instaladas; requisito integral/Procuras permanecem parciais. Fase 3 e instalação em homologação/produção dependem de autorização expressa própria. Não foram iniciados catálogo público, NDA, comprovativos, fotografias ou validade. Próximo passo é rever esta entrega e conferir visualmente quando o ambiente permitir, sem reabrir provas técnicas aceites.
|
||||
# 14/11/2026 — Ofertas, Fase 3 em QA (RF-MN-003)
|
||||
|
||||
Base F2 `91c18573`, F1 `1f2990e4` e plano `6e302db6` preservados. Consulta `/ofertas`, detalhe, API e CSV reutilizam filtros, paginação, mapa e layout público existentes. Categoria, província/distrito e preço são aplicados à projecção pública aprovada/publicada, separada da edição privada; intervalos e ordenação exigem moeda, sem câmbio. Mapas apresentam limites administrativos, sem pontos dos negócios. Contactos, documentos e dados financeiros privados não são seleccionados pelo consumidor. Sem reorganização do menu, novas tabelas, alterações em `mne`, instalações ou dados operacionais.
|
||||
|
||||
A publicação efectiva permanece bloqueada, incluindo rotas alternativas. O catálogo normal fica vazio/detalhe 404. Apenas injecção interna de teste, com a guarda de ambiente testing/base isolada em 55441, permite demonstrar projecções sintéticas; parâmetros HTTP e JSON armazenado não abrem a barreira. Não se declarou uma publicação real nem se implementaram as Fases 4/5. Uma revisão privada pendente/rejeitada não substitui o snapshot público anterior.
|
||||
|
||||
QA final: 10 testes/133 asserções aprovados; inclui acessos directos, estados inválidos, isolamento, preservação, hierarquia, decimais/moedas, mapa sem localização exacta, CSV, HTML compilado e regressão nativa autocontida. Passagem anterior: 36/41 aprovados; cinco casos nativos impedidos por referência Inhambane ausente na base sintética. Tipos, lint focado, Pint, PHPStan e build aprovados. Formatação global: os mesmos 61 ficheiros exteriores ao delta. Conferência visual/móvel pendente: `The browser access host is unavailable`; não substituída por aprovação técnica. Email real F2 mantém pendência.
|
||||
|
||||
Estado: implementado em QA com barreira fechada, não instalado; módulo integral e Procuras parciais. Limites representados: 1.000 candidatos e contexto de 30 territórios, assinalados quando truncados. Relatório: `docs/investmoz/v2/auditoria-portal/ofertas-fase3-entrega-qa-20261114.md`. Recibo: `outputs/offers-public-phase3-20261114/qa-result.json`. Próximo passo: conferência desktop/móvel e apreciação da entrega QA; sem instalação ou avanço de fase por inferência.
|
||||
|
||||
## 14/11/2026 — Ofertas Fase 4, particularidades e confidencialidade em QA
|
||||
|
||||
Base F3 7691cab3 preservada, tal como F1/F2 e plano aceite. A Tempo confirmou os campos propostos como opcionais na preparação; confirmou que o NDA real continua pendente. Empresas: sectores do cadastro existente, receita/fluxo de caixa, moeda, motivo e disponibilidade declarada. Imóveis: modalidade, tipo existente, áreas/unidade, divisões, comodidades e disponibilidade. Reutiliza localização, rascunho, versões, submissão e revisão existentes.
|
||||
|
||||
Fotografias: máximo de seis activas, envio/alteração/retirada/restauro sob autorização e bloqueio do registo. Originais cifrados no disco privado; MIME, conteúdo, tamanho e quarentena no serviço existente. Declaração de edição digital conservada. Comprovativos imobiliários só podem ser lidos por validador interno com âmbito, MFA e sem conflito; dono/interessado/auditor/outro âmbito recusados. O controlo comum de binários impede atalhos; o envio não certifica titularidade.
|
||||
|
||||
Controlo empresarial exige aceitação do exemplar exacto do NDA e autorização expressa do vendedor para a mesma versão aprovada. Cada consulta revalida ambas, direitos e revisão de acesso. Retirada/revogação e alteração de versão fecham a consulta seguinte; nova aceitação exige nova autorização. História e auditoria preservam actores, instantes, hashes, versões e antes/depois; fundamentos cifrados. A leitura autorizada só entrega os campos empresariais conferidos, sem contactos, morada exacta ou comprovativos. NDA configurado nulo, sem texto inventado ou publicação. Fotografias originais continuam privadas.
|
||||
|
||||
Os cinco testes territoriais foram investigados: dependiam de Inhambane/turismo previamente importados numa base vazia. O fixture cria apenas os mínimos sintéticos quando faltam, com guarda testing/loopback/*_test:55441 e transacção de rollback. Os seis casos passaram; nenhum cadastro operacional foi alterado. Os snapshots F4 vinculam particularidades/recursos ao contrato v3; Ofertas comuns anteriores conservam v2.
|
||||
|
||||
Estado: implementado em QA, sem instalação, publicação efectiva, pagamentos, destaque comercial, migrações ou alterações em mne. Fase 5 não iniciada; módulo integral/Procuras parciais. Pendências: exemplar/versão/aprovação do NDA, identificação jurídica dos comprovativos admitidos, visual desktop/móvel e email real anteriores. GD ausente: derivadas do processador nativo continuam bloqueadas, sem simular processamento. Formatação global mantém 61 ficheiros exteriores ao delta. Relatório: docs/investmoz/v2/auditoria-portal/ofertas-fase4-entrega-qa-20261114.md. Recibo: outputs/offers-private-phase4-20261114/qa-result.json; contém resultados e hashes finais, sem somar execuções sobrepostas. Próximo passo: apreciação QA e disponibilização do NDA aprovado, mantendo o bloqueio de publicação.
|
||||
|
||||
## 14/11/2026 — Ofertas Fase 5, fecho operacional em QA
|
||||
|
||||
Base aceite com ressalvas e431de8b preservada, tal como 7691cab3, 91c18573, 1f2990e4 e plano6e302db6. F5 autorizada exclusivamente em QA. O dono pode retirar disponibilidade imediatamente, sem aprovação editorial ou bloqueio por etag antigo. Retirada/expiração excluem leituras públicas e fecham dados protegidos por NDA. Conteúdos, identificadores, originais e decisões não são apagados.
|
||||
|
||||
Reactivar exige pedido no mesmo cadastro, novo ciclo operacional e nova submissão/revisão/aprovação no motor existente; só depois o dono repõe disponibilidade privada. O snapshot v4 inclui o ciclo, preservando v2/v3 anteriores. Uma publicação antiga não reaparece e grants NDA antigos não reabrem por causa dessa reactivação. A reposição de disponibilidade não publica.
|
||||
|
||||
Validade independente do NDA: validity_days e validity_policy_reference inicialmente nulos, sem prazo definitivo presumido. Política congelada por hash; início na publicação da versão; configuração ou aprovação privada posterior não prolongam o exemplar público anterior. Expiração verificada em cada leitura, sem depender do agendador. API/cartão apresentam data apenas quando conhecida; CSV conserva o formato anterior e a mesma exclusão. Comando investmoz:offers-expire confere sem escrita; --confirm materializa com auditoria/idempotência. Agendamento permanece desligado.
|
||||
|
||||
Denúncias fundamentadas ficam cifradas com origem, actor e versão, ligadas a tarefas CRM e ao Dashboard Colaborativo existentes. A entrada Ofertas — acompanhamento usa a mesma listagem privada e filtro de denúncias pendentes. Validador no âmbito, com MFA e sem conflito, analisa e conclui com fundamento, com ou sem retirada. Etags e versão da denúncia protegem a decisão; o proprietário não recebe os dados originais do denunciante. Retomas devolvem o recibo sem outra tarefa. Planos administrativos de destaque têm versão/histórico e só rascunho/arquivo, sob Publicador autorizado; não alteram is_featured, cobram ou executam pagamentos.
|
||||
|
||||
Verificação: percurso novo inicialmente com 12 testes/151 asserções aprovados; regressão integrada final com 59 testes/780 asserções aprovados, com resultados no recibo outputs/offers-private-phase5-20261114/qa-result.json. Grupos sobrepostos não devem ser somados. Relatório técnico e matriz de aceitação das cinco fases: docs/investmoz/v2/auditoria-portal/ofertas-fase5-fecho-qa-20261114.md. A matriz RF-MN-003 existente foi actualizada, sem novo inventário geral.
|
||||
|
||||
Estado: cinco fases preparadas em QA; F1-F4 aceites tecnicamente com ressalvas, F5 para apreciação; nenhuma instalação ou publicação efectiva. Mantêm-se pendências NDA/exemplar/versão/aprovação, tipos jurídicos de comprovativos, prazo final, avisos jurídicos, visual desktop/móvel, GD/WebP e email real F2. Formatação global mantém61ficheiros preexistentes fora do delta. NDA mantém configuração nula; sem migrações, alterações em mne, dados operacionais, pagamentos ou promoção comercial. Procuras/restante portal não encerrados por este lote. Próximo passo: apreciação QA e decisões documentais; instalação/activação/publicação aguardam autorização expressa.
|
||||
# Auditoria de aceitação Ofertas — 14/11/2026, antes de instalar
|
||||
|
||||
Referência: `../v2/auditoria-portal/ofertas-aceitacao-final-20261114.md`. Candidata `528a2597`; commits e hashes F1–F5 conferidos, JUnit aceite 59 testes/780 asserções confirmado sem o apresentar como nova execução. Nova regressão dos componentes nativos: 14/227 aprovada. Novo teste de integração: 2/11, um aprovado e uma falha na reutilização da descrição da Oferta pelo contexto de investimento; recuperação financeira privada e isolamento passaram. A aceitação integral permanece bloqueada: falta demonstrar Oferta → dossier → viabilidade em função da localização → relatório → recuperação. Publicação efectiva não implementada/activada; NDA real, visual/móvel, imagens e entrega real de email mantêm os estados específicos do relatório. Plano de instalação preparado, zero novas migrações no delta, schema do destino e reversão compatível por conferir. Nenhuma instalação ou alteração funcional efectuada nesta auditoria. RF-MN-003 actualizado sem alterar os restantes requisitos.
|
||||
# Correcção Ofertas → contexto de investimento — QA, 14/11/2026
|
||||
|
||||
Após `ed2fd86f`, foi autorizada exclusivamente a correcção da continuidade. `InvestorContext` passa a recuperar a descrição da Oferta quando não existe resumo, mantendo a mesma Opportunity, os identificadores e a autorização privada. Resumo existente e edições deliberadas no dossier prevalecem. Não se copiam dados empresariais confidenciais, contactos ou preço privado; NDA/autorização do vendedor não concedem acesso ao contexto privado. A prova sintética percorre criação, submissão, revisão/aprovação, projecção pública isolada de teste, contexto/dossier, localização, pressupostos financeiros declarados, gravação/recuperação e relatórios HTML/Word. Não representa publicação real nem rentabilidade garantida. Recibo e resultados em `../v2/auditoria-portal/ofertas-correcao-contexto-qa-20261114.md` e `outputs/offers-investment-continuity-20261114/qa-result.json`; RF-MN-003 e matriz final actualizados. Mantêm-se pendências jurídicas, visuais, de imagens e email real. Sem instalação, activação pública, migrações ou alteração de dados operacionais; aguarda nova autorização para instalar.
|
||||
# Preparação final de homologação privada das Ofertas — 14/11/2026
|
||||
|
||||
A Tempo aceitou tecnicamente `9a014268` e autorizou apenas preparação. Parecer em `../v2/auditoria-portal/ofertas-prontidao-homologacao-20261114.md`: integração privada em QA demonstrada, 48 hashes dos recibos F5/correcção conferidos e bundle Git local da candidata verificado. Sem diferenças de migrações/locks face ao recibo histórico `96f51096`; destino actual ainda por conferir. Homologação privada recomendável com publicação fechada, condicionada a revisão/schema/runtime reais, backup/restauro actual, delta e reversão compatível, e nova autorização expressa. GD/WebP ausente no PHP QA bloqueia derivadas, preservando originais; visual/móvel, email real e decisões jurídicas mantêm pendências próprias. Ensaio de correio preparado com dados sintéticos e caixa de teste a designar, sem fila/destinatários operacionais. Nenhuma instalação, migração, alteração operacional, envio de email ou activação pública nesta preparação. RF-MN-003 e matriz final distinguem aprovação técnica, instalação e publicação.
|
||||
# Menus das Ofertas uniformizados na prévia QA — 14/11/2026
|
||||
|
||||
Por pedido expresso da Tempo durante a conferência, a navegação privada das Ofertas reutiliza o padrão horizontal com dropdown do portal. Área de trabalho → Ofertas agrupa consulta/preparação conforme os acessos existentes; o módulo agrupa Ofertas, estados e acompanhamento em dropdowns responsivos. Mudar de estado conserva os filtros de acompanhamento/denúncias. Todas as páginas Ofertas usam o mesmo enquadramento PortalNavigation. Não foram alteradas permissões, rotas, publicação ou dados. Passaram 29 testes de navegação, tipos, lint focado e build QA; um aviso de lint preexistente fora do delta foi preservado. A lista e os dois exemplos já existentes responderam HTTP 200 com os novos assets, mantendo a sessão e os dados da prévia. Conferência visual pelo utilizador pendente; sem instalação em homologação. Recibo: `outputs/offers-menu-qa-20261114/qa-result.json`. O bundle anterior `9a014268` permanece preservado e não inclui este novo delta visual.
|
||||
# Verificação real pré-instalação das Ofertas — 14/11/2026
|
||||
|
||||
Por autorização específica, destino `teste-investidor.tempo.co.mz` confirmado em `96f51096`, sem instalar Ofertas ou alterar configuração/permissões/dados operacionais. Estruturas verificadas compatíveis; GD/WebP presente em CLI/FPM e codec em memória aprovado. Backup actual privado e restauro isolado passaram: 219 tabelas, 417 ficheiros, dois conjuntos de MFA; zero originais Laravel cifrados na fonte para ensaiar. Release anterior arquivada e 9.873 ficheiros reconferidos. Após a cópia, apenas uma sessão nova, com as 51 anteriores intactas; nenhuma outra tabela divergente. Parecer APTO COM CONDIÇÕES: autorização futura com SHA expresso (base `9a014268` ou candidata com menus `3558647e`), reconferência da salvaguarda e janela sem integrações concorrentes; o executor segue o último `main`. Publicação/NDA/agendamento/correio de Ofertas mantidos fechados; visual, galeria completa, email real e decisões jurídicas permanecem separados. Relatório: `../v2/auditoria-portal/ofertas-verificacao-preinstalacao-20261114.md`; recibos sanitizados em `outputs/offers-preinstall-verification-20261114`. Não houve instalação, migração operacional, alteração de `mne` ou envio real de email.
|
||||
# Instalação autorizada das Ofertas — paragem de entrada, 14/11/2026
|
||||
|
||||
A Tempo autorizou expressamente `3558647e`, incluindo menus, mantendo fecho público. A verificação de entrada encontrou `main` e homologação em `96f51096`; o executor segue esse ramo. Em cumprimento da condição 4, a operação foi interrompida antes de qualquer push/pedido/instalação. Executor inactivo, zero pedidos; nenhuma migração, alteração operacional ou reversão. A instalação já está autorizada, mas a integração exacta e controlada de `3558647e` em `main` exige decisão após esta paragem; o fast-forward é possível sem força e pode accionar a instalação. Não reiniciar preparação/testes aceites para resolver esta diferença de ramo. Recibo: `outputs/offers-authorised-install-20261114/entry-blocked.json`.
|
||||
# Instalação exacta das Ofertas — falha antes da promoção e correcção QA, 14/11/2026
|
||||
|
||||
O fast-forward autorizado `96f51096 → 3558647e` foi efectuado sem força ou commits posteriores. Janela com path automático pausado, lock exclusivo e chamada única ao executor existente, recusando qualquer outro SHA. Novo backup/restauro isolado 219 tabelas/417 ficheiros aprovado. O executor passou schema isolado 1/3106 e build, mas parou em tipos: colisão Wayfinder entre a acção GET `reportForm` e o formulário gerado para POST `report`. Sem promoção, migrações, alteração operacional ou reversão; homologação servida permanece `96f51096`, 219 tabelas/417 ficheiros reconferidos intactos. Lock libertado, path original restaurado, fila vazia. Correcção mínima em QA: acção GET `reportPage`, mesmo URL e corpo, com geração real/produção/tipos/PHPStan/Pint e 28 testes/403 asserções aprovados, numa base isolada separada da prévia. Nenhum novo push ou instalação. Relatório: `../v2/auditoria-portal/ofertas-instalacao-privada-incidente-wayfinder-20261114.md`; recibo `outputs/offers-exact-install-20261114/qa-result.json`. Nova revisão correctiva exige autorização expressa antes de integrar/instalar; publicação, correio, pagamentos e decisões jurídicas continuam nos limites anteriores.
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,70 @@
|
||||
# Ofertas — auditoria final antes da instalação
|
||||
|
||||
14/11/2026. Candidata auditada: `528a2597`. Auditoria exclusiva em QA, por instrução da Tempo. Nenhuma instalação, activação pública, migração operacional ou alteração funcional realizada nesta passagem.
|
||||
|
||||
**Decisão: aprovação técnica parcial com ressalvas; aceitação integral bloqueada.** As provas das cinco fases são válidas para os comportamentos que demonstram. Não demonstram uma publicação operacional nem a continuidade completa Oferta → dossier → viabilidade territorial. Foi reproduzida uma perda de reutilização da descrição nessa fronteira.
|
||||
|
||||
**Actualização posterior à auditoria `ed2fd86f`:** a correcção autorizada em QA está documentada em [Ofertas → Investimento](ofertas-correcao-contexto-qa-20261114.md). O saldo actualizado nas linhas abaixo substitui apenas o estado desses critérios; conserva-se a prova original da falha. O percurso controlado com hipóteses de localização e relatório foi demonstrado; publicação operacional e restantes dependências não foram activadas.
|
||||
|
||||
**Preparação final após aceitação técnica expressa de `9a014268`:** [parecer de homologação privada](ofertas-prontidao-homologacao-20261114.md). Integridade das fases/correcção reconfirmada por 48 hashes; bundle local verificado. Aprovação técnica em QA aceite; instalação ainda não autorizada/executada, condicionada à conferência real do destino e backup/restauro actual. Publicação operacional permanece bloqueada. Visual, GD/derivadas e email real são critérios de avaliação pendentes, sem invalidar a prova integrada privada aceite.
|
||||
|
||||
**Destino e salvaguarda posteriormente verificados por autorização específica:** [verificação pré-instalação](ofertas-verificacao-preinstalacao-20261114.md), **APTO COM CONDIÇÕES**. Schema/runtime compatíveis, GD/WebP do destino disponível, restauro isolado de 219 tabelas/417 ficheiros aprovado. Instalação e publicação continuam não autorizadas; a instalação exige SHA expresso e janela sem integrações concorrentes porque o executor segue o último `main`. Menus `3558647e` constituem delta QA posterior à candidata `9a014268`.
|
||||
|
||||
## Evidência e limites
|
||||
|
||||
- Confirmada a ancestralidade dos commits F1 `1f2990e4`, plano `6e302db6`, F2 `91c18573`, F3 `7691cab3`, F4 `e431de8b` e F5 `528a2597`.
|
||||
- Conferidos todos os hashes de código e provas do recibo F5, antes das alterações documentais desta auditoria: todos correspondem. O JUnit original confirma **59 testes, 780 asserções, zero erros/falhas**. Não se apresenta esta conferência como nova execução dos 59 testes.
|
||||
- Nova execução de `OpportunityManagementTest`, `OpportunitySearchTest` e `OpportunityResourcesTest`: **14 testes / 227 asserções aprovados**, na candidata, para compatibilidade com cadastro nativo, versões públicas, pesquisa/lista/mapa/exportação, geografia e recursos.
|
||||
- Novo `OfferInvestmentAcceptanceAuditTest`: **2 testes / 11 asserções; 1 aprovado e 1 falhado**. Recuperação financeira privada e isolamento na selecção de contexto passaram; reutilização da descrição falhou. A primeira execução revelou a chave ausente; a segunda tornou a observação numa asserção explícita, sem alterar o produto.
|
||||
- Base exclusiva `offers_phase1_20261114_test`, loopback:55441, utilizador de testes existente e dados sintéticos; guardas do repositório preservadas. Não se executaram suites fixadas noutra base nem se enfraqueceram as respectivas guardas.
|
||||
- Recibo: `outputs/offers-final-acceptance-20261114/receipt-verification.json`; execuções adicionais: `native-integration.xml/log` e `investment-acceptance.xml/log`, na mesma pasta. Não somar as asserções das várias passagens como medida de conclusão funcional.
|
||||
- A compilação, PHPStan, tipos e lint focado aceites são reutilizados: não se alterou código funcional. Os 61 ficheiros com formatação preexistente foram novamente comparados por caminho e conteúdo Git: todos permanecem inalterados e fora do delta das cinco fases. Prova adicional em `matrix-and-format-verification.json`; isto não constitui aprovação de formatação global. O novo teste passou na verificação sintáctica PHP e no Pint; `git diff --check` passou.
|
||||
|
||||
## Matriz final consolidada
|
||||
|
||||
| Percurso / requisito | Estado | Evidência e limite concreto |
|
||||
| --- | --- | --- |
|
||||
| F1: criação, gravação, recuperação, edição e submissão privada no cadastro existente | aprovado com ressalvas | Suite F1, 14/146; mesma identidade e etags. Conferência visual pendente. |
|
||||
| F2: submissão, revisão, rejeição fundamentada, correcção, reenvio e aprovação independente | aprovado com ressalvas | Suite F2, 11/161; decisões por snapshot, conflitos, tarefas, auditoria, autor/contribuidor sem auto-aprovação. Email real e visual pendentes. |
|
||||
| Separação aprovação/publicação e bloqueios dos percursos alternativos | aprovado | Testes F2 e barreira do modelo/consumidores; aprovação privada não publica. |
|
||||
| F3: filtros, listagem, detalhe, API, mapa e CSV | aprovado com ressalvas | Suite F3, 10/133, projecções exclusivamente sintéticas; moeda sem conversão, coordenadas administrativas. Limites 1.000 candidatos/30 territórios e visual por conferir. |
|
||||
| Publicação controlada operacional | bloqueado | Não existe publicador efectivo das Ofertas. `OfferPublicCatalogue` só aceita projecções de teste por injecção interna em base isolada. Não é uma configuração pública pronta a ligar. |
|
||||
| F4: campos de empresas/imóveis, recuperação e vínculo à versão | aprovado com ressalvas | Suite F4, 11/160; campos opcionais confirmados. Não certifica titularidade nem demonstra cálculo de rentabilidade. |
|
||||
| Seis fotografias, ficheiros, quarentena e comprovativos internos | aprovado com ressalvas | Upload/limites/autorização/scan/cifra testados; processamento e galeria com derivadas não demonstrados por ausência de GD/WebP no QA. |
|
||||
| NDA + autorização do vendedor + revogação + mudança de versão | aprovado com ressalvas | Mecanismo técnico testado com documentos sintéticos e revalidação em cada leitura. NDA aprovado real ausente; configuração nula mantém fechado. |
|
||||
| F5: retirada imediata, expiração, revalidação e reactivações | aprovado com ressalvas | Suite F5, 13/180; fronteira temporal, idempotência, ciclo novo e grants antigos recusados. Prazo definitivo ainda por decidir; não se inventou duração. |
|
||||
| Denúncias e tratamento no Dashboard, auditoria e destaques administrativos | aprovado com ressalvas | F5 cobre isolamento, fundamento cifrado, tarefas existentes, versões e responsável. Destaque só rascunho/arquivo, sem promoção/pagamentos. Visual pendente. |
|
||||
| Isolamento de utilizadores, páginas, APIs, anexos, mapas e CSV | aprovado com ressalvas | Suites F1–F5 e regressão nativa; leitura pública limitada à projecção, comprovativos internos, sem originais externos. Não equivale a teste de intrusão independente nem validação visual. |
|
||||
| Preservação da versão pública durante edição | aprovado com ressalvas | Contrato sintético de Ofertas e nova regressão nativa aprovados; não existe ainda versão pública operacional de Ofertas para demonstrar o percurso real. |
|
||||
| Escrever uma vez: recuperar a descrição no contexto de investimento | aprovado | Correcção QA específica da transformação; descrição recuperada, resumo existente e alterações deliberadas preservados, sem truncar a origem nem duplicar registos. Prova e recibo no relatório de correcção. |
|
||||
| Contexto territorial e hipóteses económicas autorizadas até à viabilidade | aprovado com ressalvas | Localização persistida até ao rascunho, custos de renda/transporte declarados pelo investidor, cálculo e comparação sensível aos custos. Valores empresariais confidenciais não são transferidos; não constitui conclusão automática sobre rentabilidade nem valida todos os cenários fiscais. |
|
||||
| Percurso controlado QA Oferta → mesmo dossier → simulação → relatório → recuperação | aprovado com ressalvas | Criação/submissão/revisão/aprovação reais no teste, projecção pública sintética isolada, referência privada autorizada, rascunho/resultados no dossier e exportadores HTML/Word existentes. Abertura pública operacional continua bloqueada e visual pendente. |
|
||||
| Restantes oportunidades, pesquisa e recursos afectados | aprovado com ressalvas | Nova regressão 14/227 passou. Não constitui declaração de ausência de defeitos em todo o portal ou em serviços externos não ensaiados. |
|
||||
| Desktop/móvel e acessibilidade funcional | não verificado | Última evidência do conector: `The browser access host is unavailable`; não foi repetido o ciclo de reinícios. HTML compilado não substitui conferência visual. |
|
||||
| Entrega real de notificações por email | não verificado | Serviço existente e permissões foram testados com transporte de teste; SMTP/fila/recepção real não ensaiados. Não foram enviados emails operacionais. |
|
||||
|
||||
## Bloqueantes e ressalvas para decisão
|
||||
|
||||
1. **Integração funcional — corrigida em QA na passagem autorizada posterior:** o teste da descrição e a prova controlada contexto/dossier/viabilidade/relatório/recuperação passaram. A projecção limita-se a campos comuns autorizados; dados económicos confidenciais não são transferidos. Moeda e custos do investimento são hipóteses explícitas, sem confundir preço de aquisição com investimento total ou dados históricos com previsão. Qualquer novo contrato de transferência confidencial continua fora desta correcção.
|
||||
2. **Publicação:** implementar e testar um publicador controlado, se autorizado em nova instrução. A aceitação do consumidor sintético não autoriza remover as barreiras existentes. Instalação privada de avaliação e abertura pública são decisões separadas.
|
||||
3. **Jurídico:** faltam exemplar/versão/referência de aprovação do NDA, tipos admissíveis de comprovativo imobiliário, política final de validade e condições/avisos aplicáveis à divulgação. Não são motivos para invalidar as provas técnicas já aceites; bloqueiam os comportamentos dependentes.
|
||||
4. **Imagens:** comprovar processamento seguro e derivadas numa instalação compatível, incluindo scan, metadados, acesso e limite de seis. Não instalar extensões nem declarar galeria pronta nesta auditoria.
|
||||
5. **Visual e correio:** realizar conferência real desktop/móvel e entrega de email autorizada em ambiente de teste. Permanecem não verificados, sem solicitar que o utilizador repita autenticações nesta passagem.
|
||||
6. **Reversão:** um retorno cego à F4 é inseguro como plano: a versão F4 de `PrivateOfferConfidentiality::approvedHash` não verifica `OfferAvailability`, enquanto a F5 o faz. Uma retirada de ciclo zero pode conservar o mesmo hash editorial. A reversão deve preservar a recusa de leitura de ofertas retiradas/expiradas e os históricos; não assumir que repor código antigo mantém as novas protecções.
|
||||
|
||||
Na auditoria original não foram corrigidas lacunas por estar autorizada exclusivamente a auditoria. A falha histórica foi conservada; a autorização posterior permitiu a correcção delimitada agora comprovada, sem alterar os restantes bloqueios.
|
||||
|
||||
## Plano de instalação controlada — não executado
|
||||
|
||||
1. **Antes de executar:** a Tempo aceitou tecnicamente a correcção `9a014268`. Para avaliação estritamente privada, existe parecer favorável condicionado e bundle local verificado; conservar NDA nulo, publicação indisponível, prazo pendente e agendamento desactivado. Conferir o destino, delta, backup/restauro e compatibilidade da reversão; obter autorização expressa para instalar. Não repetir provas aceites sem diferença que o justifique. Isto não constitui aceitação de publicação integral.
|
||||
2. **Destino e processo:** confirmar `teste-investidor.tempo.co.mz`, revisão efectivamente instalada, ramo, árvore limpa, recibo do executor Git existente e diferença exacta desde o activo. Não usar o destino antigo do guia genérico como evidência do destino actual. Não inventar comandos/identidade de executor nem retomar uma sessão SSH histórica. Esta auditoria não consultou o servidor.
|
||||
3. **Estrutura:** o delta completo `1f2990e4^..528a2597` contém **zero ficheiros de migração**. As fases reutilizam Opportunity/JSON, recursos/documentos, perfis, editorial, tarefas e auditoria. Antes de instalar, comparar em leitura o schema e o registo de migrações do destino com estas dependências. Ausência de nova migração no pacote não prova que o destino já possui todas as estruturas; qualquer falta exige plano aditivo concreto e autorização. Não executar `migrate:fresh`, seeders ou alterações a `mne`.
|
||||
4. **Cópia e ensaio:** backup consistente da base, ficheiros privados, configuração e chaves existentes sob protecção; registar revisão e manifestos sem segredos. Ensaiar restauro isolado, integridade de anexos cifrados e leitura de histórico, comparando contagens/hash e identificadores com o backup. Preservar integralmente instituições, demografia, dossiers e integrações; não presumir contagens actuais a partir de recibos antigos.
|
||||
5. **Instalação após autorização expressa:** executar apenas o manifesto revisto pelo processo Git existente; compilar com versões fixadas, preservar segredos/configuração, rever caches/workers segundo o executor aceite. Sem publicação automática, concessões de perfis, pagamento, destaque comercial ou activação do agendamento. Não copiar exemplares NDA sintéticos para configuração operacional.
|
||||
6. **Verificações posteriores:** conferir SHA/schema/delta, prontidão e logs sem segredos; acessos válidos e inválidos, recuperação de oferta e versões, ausência no catálogo nativo/público, ficheiros reservados, denúncias e tarefas, regressão dos destinos afectados. Testes de escrita apenas em contexto sintético autorizado e identificado; não submeter processos reais. Conferência desktop/móvel e, se autorizado, email de teste com destinatário controlado. Registar quem verificou, versão e resultado.
|
||||
7. **Reversão:** guardar revisão anterior e artefactos, mas condicionar reversão de código à compatibilidade com v2/v3/v4 e novas decisões operacionais. Usar uma revisão de recuperação que conserve as guardas F5; se indisponível, não abrir os percursos afectados. Não remover JSON, tarefas, denúncias, decisões ou grants para acomodar código antigo. Não restaurar indiscriminadamente a base sobre novos registos; reconciliação/restauro de dados exige decisão própria e preservação do histórico.
|
||||
8. **Aprovação separada:** apresentar resultado e recibo pós-instalação. Qualquer futura publicação efectiva requer desenvolvimento/provas e autorização próprios. Este plano não é autorização para instalar nem para activar.
|
||||
|
||||
## Resultado e próxima acção
|
||||
|
||||
Auditoria original concluída com achados; a continuidade Oferta → contexto/dossier e a prova controlada de viabilidade/relatório foram corrigidas em QA por autorização posterior. **O módulo mantém ressalvas e bloqueios de publicação operacional, jurídicos e verificações pendentes.** Próxima acção: apreciar o recibo de correcção e as dependências restantes antes de qualquer autorização de homologação. Nenhuma instalação efectuada.
|
||||
@@ -0,0 +1,72 @@
|
||||
# Ofertas → Investimento — correcção da continuidade em QA
|
||||
|
||||
14/11/2026. Base: auditoria `ed2fd86f`, preservando F1–F5 até `528a2597`. Autorização exclusiva para investigar/corrigir a continuidade e demonstrá-la em QA. Sem instalação, publicação efectiva, alterações operacionais, migrações ou intervenção em `mne`.
|
||||
|
||||
## Causa e correcção
|
||||
|
||||
A reprodução antes da alteração confirmou o resultado anterior: dois testes, um aprovado e um falhado; o contexto não tinha `project_summary`. A descrição estava correctamente persistida em `Opportunity.description`, no mesmo identificador. A autorização já permitia ao dono consultar a Oferta. A perda ocorria na **transformação/recuperação**: `InvestorContext::reference('opportunity:…')` só projectava `canonical.executive_summary` ou `summary`, enquanto a preparação privada das Ofertas escreve `description`.
|
||||
|
||||
O adaptador existente passa a usar a descrição guardada como alternativa para `project_summary`, **apenas para Ofertas privadas já autorizadas**, conservando qualquer resumo existente não vazio. O título vem dos valores correntes da mesma preparação e a referência aponta para a rota específica da Oferta. Não se criam registos, fontes, relações ou contratos paralelos. Não se trunca a descrição, não se grava uma cópia na Oportunidade e não se altera a origem ao confirmar dados no dossier.
|
||||
|
||||
O reconciliador existente continua a apresentar prévia e confirmação, com versões, hash, proveniência e preservação de edições deliberadas. O teste integrado confirma que um resumo deliberadamente alterado no dossier não é substituído numa nova prévia da mesma Oferta.
|
||||
|
||||
## Campos e confidencialidade
|
||||
|
||||
| Informação | Continuidade autorizada |
|
||||
| --- | --- |
|
||||
| Identificador/chave da Oportunidade, versão, hash, título e referência | Contrato existente `opportunity:<id>`; mesmo cadastro e dossier, sem duplicação. |
|
||||
| Descrição | Alternativa para resumo apenas no contexto privado autorizado; resumo existente prevalece. Descrição longa preservada. Não é uma autorização de divulgação pública. |
|
||||
| Referências administrativas e sector comum já cadastrado | Contratos existentes preservados; a localização do ensaio mantém o mesmo identificador até ao contexto e ao rascunho financeiro. Não se deduz cobertura, disponibilidade ou benefício fiscal. |
|
||||
| Moeda, custos e pressupostos do investimento | Declarações explícitas do investidor no percurso existente. A moeda/preço da Oferta não se convertem automaticamente em moeda/investimento total do projecto. |
|
||||
| `private_offer.business`, receitas, fluxo de caixa, preço privado, contacto, razões e comprovativos | Não são copiados pelo adaptador para o contexto, relatórios ou exportações. Os campos de sector guardados dentro da informação empresarial confidencial também não são promovidos ao sector comum por inferência. |
|
||||
| Informação pública sintética | Apenas projecção pública separada e expressamente construída no teste. A descrição privada e os valores empresariais não passam para essa projecção. |
|
||||
| Leitura por NDA e autorização do vendedor | Mantém o âmbito próprio: não concede acesso à referência privada do contexto. Novo teste confirma recusa 404 mesmo depois das duas condições satisfeitas; revogação e demais testes F4 permanecem válidos. |
|
||||
|
||||
Não se introduziu um novo percurso de transferência de dados confidenciais. Qualquer futura reutilização desses dados precisa de autorização e contrato próprios; esta correcção não alarga direitos de consulta ou exportação.
|
||||
|
||||
## Prova integrada reproduzível
|
||||
|
||||
`OfferInvestmentAcceptanceAuditTest` usa exclusivamente `offers_phase1_20261114_test`, loopback:55441, com guardas e rollback existentes. Cria Oferta de empresa e referência provincial sintéticas, grava/recupera, associa contacto autorizado, submete, efectua revisão/aprovação por outra conta e conserva `status=draft`/`published_at=null`.
|
||||
|
||||
O mesmo teste comprova catálogo normal vazio e injecta `OfferPublicCatalogue(true)` exclusivamente no contentor do teste, sujeito à guarda real da base isolada. A projecção aprovada/publicada sintética tem resumo público próprio; não se altera qualquer interruptor de publicação operacional. A referência privada continua sujeita a `PortalProfileAccess`.
|
||||
|
||||
O dono selecciona a mesma Oportunidade num dossier, confere a prévia e confirma. Descrição e referência provincial chegam ao prefill existente do simulador. O investidor declara moeda e pressupostos de renda/transporte para esse local; guarda o rascunho financeiro, recupera-o e devolve os resultados com confirmação da revisão do contexto. Uma nova autenticação da mesma conta recupera o mesmo dossier e rascunho; outra conta é recusada. O teste confirma uma Oportunidade e um dossier, sem duplicações.
|
||||
|
||||
O teste frontend consome o JSON produzido pela prova PHP, recupera o snapshot e executa os calculadores existentes: receita mensal sintética 10.000, renda 2.000 e transporte 1.000 produzem saldo operacional estimado de 7.000 MZN; custos alternativos expressamente declarados de 5.000 e 3.000 produzem 2.000 MZN. Custos fixos 3.000, preço unitário 100 e custo variável 50 dão ponto de equilíbrio de 60 unidades. Estas hipóteses demonstram sensibilidade aos custos do local; **não atribuem uma tarifa provincial, uma margem real da empresa ou rentabilidade garantida**. Receita histórica confidencial da Oferta não é usada como previsão.
|
||||
|
||||
O mesmo teste gera o relatório privado HTML e Word pelo exportador existente, reabre o Word e verifica descrição/localização, recuperação do snapshot e ausência dos valores confidenciais e contacto. Os artefactos são sintéticos, não documentos de negócio nem uma aceitação visual.
|
||||
|
||||
Durante a preparação da prova, a referência provincial inicial não tinha a proveniência exigida pelo catálogo público; foi corrigida apenas a fixture isolada, reutilizando o contrato de `OpportunitySearchFixture`. O pedido do simulador foi ajustado à lista de campos já aceite e à confirmação da revisão de contexto. Nenhuma guarda foi desactivada para passar o ensaio.
|
||||
|
||||
## Ficheiros e verificação
|
||||
|
||||
- Produto: `app/Services/InvestorContext.php`, projecção delimitada de descrição/título/referência.
|
||||
- Testes PHP: `OfferInvestmentAcceptanceAuditTest.php`, persistência, resumo existente, descrição longa, dossier/rascunho/resultados e isolamento; `PrivateOfferConfidentialityTest.php`, acesso por NDA sem concessão de acesso ao contexto privado.
|
||||
- Teste frontend: `tests/frontend/offer-investment-continuity.test.mjs`, cálculo com pressupostos territoriais declarados e relatório real com reabertura do Word.
|
||||
- Documentação: matriz RF-MN-003, manual e matriz final de aceitação existentes; este recibo explica a correcção sem apagar a prova histórica da falha.
|
||||
|
||||
Resultados e comandos finais: `outputs/offers-investment-continuity-20261114/qa-result.json`. Não somar passagens sobrepostas nem apresentar os 59/780 históricos como executados novamente nesta correcção.
|
||||
|
||||
| Verificação final | Resultado |
|
||||
| --- | --- |
|
||||
| Continuidade Oferta/contexto e percurso integrado | 4 testes / 62 asserções aprovados |
|
||||
| Confidencialidade, incluindo NDA sem acesso ao contexto privado | 11 / 161 aprovados |
|
||||
| Percursos existentes de investimento/dossiers | 4 / 192 aprovados |
|
||||
| Gestão nativa de Oportunidades e versão pública | 3 / 46 aprovados |
|
||||
| **Passagem PHP integrada** | **22 testes / 461 asserções; zero erros/falhas** |
|
||||
| Regressão frontend de plano financeiro e relatórios | 41 testes aprovados |
|
||||
| Prova frontend ligada ao JSON da passagem PHP final | 1 teste aprovado; HTML e Word gerados/reabertos |
|
||||
| PHPStan completo, TypeScript e Pint | Aprovados; zero erros PHPStan |
|
||||
| Lint/formatação do novo teste frontend; diferenças Git | Aprovados |
|
||||
|
||||
Comando PHP final: `php vendor/bin/phpunit tests/Feature/OfferInvestmentAcceptanceAuditTest.php tests/Feature/PrivateOfferConfidentialityTest.php tests/Feature/InvestorPathTest.php tests/Feature/OpportunityManagementTest.php --log-junit outputs/offers-investment-continuity-20261114/regression.xml`, com a configuração sintética/guardada acima. Frontend: `node --test tests/frontend/investor-dossier-report.test.mjs tests/frontend/business-financial-plan.test.mjs`, seguido de `node --test tests/frontend/offer-investment-continuity.test.mjs`. Análise: `php vendor/bin/phpstan analyse --no-progress --memory-limit=1G`, `npm.cmd run types:check`, Pint nos três ficheiros PHP e `vp fmt --check`/`vp lint` no novo teste. A compilação QA anterior é reutilizada porque não mudou código frontend do produto; os testes PHP preservaram os assets existentes.
|
||||
|
||||
## Saldo para aceitação
|
||||
|
||||
**Corrigido em QA:** descrição na fronteira Oferta/contexto, preservação de resumo e edições deliberadas, recuperação no mesmo dossier, ensaio de localização/pressupostos → cálculo → relatórios HTML/Word → recuperação, sem passagem de dados financeiros confidenciais.
|
||||
|
||||
**Mantém-se bloqueado:** publicação efectiva operacional; NDA real enquanto faltar o exemplar aprovado; decisões jurídicas/prazo definitivos identificados na auditoria. A injecção de projecção sintética não substitui esse fecho.
|
||||
|
||||
**Por verificar:** conferência visual desktop/móvel, processamento de derivadas de imagens/galeria no ambiente compatível e entrega real de email. A simulação demonstrada é uma análise controlada com hipóteses declaradas, não uma prova de todos os cenários fiscais e factores de rentabilidade do portal.
|
||||
|
||||
Nenhuma instalação efectuada ou autorizada por este recibo. A eventual instalação aguarda nova autorização expressa, com o plano de cópia/restauro e reversão compatível já documentado.
|
||||
+168
@@ -0,0 +1,168 @@
|
||||
# InvesteMoz — encerramento técnico da Fase 1 e plano da Fase 2
|
||||
|
||||
Data: 14/11/2026. Referência funcional: RF-MN-003 da matriz existente.
|
||||
|
||||
**Revisão examinada: `1f2990e4`. Fase 1 verificada tecnicamente, com ressalva de conferência visual não realizada. Fase 2 apenas planeada, não autorizada para implementação. Nenhuma instalação em homologação ou produção.**
|
||||
|
||||
## 1. Resultado do encerramento técnico
|
||||
|
||||
A implementação recebida mantém Oportunidades como cadastro principal. Criar, guardar, recuperar, editar e submeter são operações privadas, com os perfis, contactos, localização, versões e auditoria existentes. A submissão não abre automaticamente revisão, aprovação ou publicação. Não foram encontradas alterações posteriores nos 16 ficheiros identificados pelos hashes do recibo, incluindo código, testes e documentação da entrega. Os 17 ficheiros do commit incluem também o próprio recibo.
|
||||
|
||||
O encerramento técnico não constitui aceite visual, aprovação da verificação global de formatação ou autorização de instalação. O módulo Ofertas integral e RF-MN-003, que também abrange Procuras, não ficam encerrados por esta fase.
|
||||
|
||||
### Evidências confirmadas
|
||||
|
||||
| Evidência | Resultado e limite |
|
||||
| --- | --- |
|
||||
| `outputs/offers-private-phase1-20261114/qa-result.json` | Recibo recebido conferido; 16 hashes correspondem aos ficheiros examinados |
|
||||
| Regressão recebida | 35 testes / 548 asserções aprovados, segundo o recibo; prova anterior reutilizada, sem repetir a bateria nesta passagem |
|
||||
| `tests-final.log` | Resultado bruto confere com o recibo: 16 testes / 172 asserções aprovados |
|
||||
| Portabilidade do HTML compilado | Recibo regista 1 teste / 10 asserções aprovado; utiliza o directório do executor e `build` por defeito |
|
||||
| Pint, PHPStan, tipos, lint focado e build QA | Aprovações do recibo reutilizadas, pois os ficheiros e dependências da candidata não mudaram |
|
||||
| Verificação global de formatação | Executada novamente nesta passagem: 61 ficheiros; todos exteriores ao delta e com o mesmo blob Git da revisão anterior |
|
||||
| Preservação | Nenhuma alteração de código, testes, configurações, permissões ou base de dados nesta passagem |
|
||||
|
||||
Os grupos de testes sobrepõem-se: não somar os totais nem tratar o número de testes como funcionalidades concluídas. A bateria canónica extensa interrompida na implementação anterior não é considerada aprovada. O teste antigo de divulgação vinculado a outra base QA conserva a sua guarda; essa base não foi alterada para repetir a prova.
|
||||
|
||||
### Cobertura de autorização e isolamento
|
||||
|
||||
| Comportamento | Evidência existente |
|
||||
| --- | --- |
|
||||
| Anónimo, email não verificado e conta sem atribuição não criam ofertas | `test_other_accounts_ungranted_unverified_and_revoked_actors_cannot_operate` |
|
||||
| Outro promotor individual não lê, edita ou submete; a listagem não revela o registo | Mesmo teste, com pedidos HTTP aos três percursos e listagem vazia |
|
||||
| Atribuição expirada deixa de autorizar acesso | Mesmo teste; a preparação específica usa expiração, não simula revogação administrativa |
|
||||
| Revogação administrativa invalida a sessão; gestor legado não contorna o âmbito | `PortalProfileAccessTest::test_revocation_invalidates_existing_session_and_legacy_manager_cannot_bypass_scope`, na regressão recebida |
|
||||
| Organização não pode ser trocada e perda da representação impede acesso/edição | `test_organization_representation_is_rechecked_and_other_organizations_are_not_assignable` |
|
||||
| Categoria não atribuída e campos de proprietário/estado/contexto forjados são recusados | `test_contact_scope_and_malicious_fields_roll_back_without_parallel_records` |
|
||||
| Contacto de outro titular é recusado; falha reverte a criação | Mesmo teste; sem oportunidade residual |
|
||||
| Gravar contacto exige MFA; contacto eliminado impede a submissão | `test_existing_contacts_registration_still_requires_fresh_mfa_and_submission_rechecks_contact` |
|
||||
| Edição concorrente não substitui dados; repetição da criação conserva a identidade | `test_private_creation_recovery_edit_conflict_and_idempotency_use_one_opportunity` |
|
||||
| Submissão exige confirmação e versão actual; não cria edição ou notificação editorial | `test_submit_is_private_exact_version_confirmed_and_recoverable_without_editorial_actions` |
|
||||
| Rotas nativas não permitem editar/divulgar/publicar a oferta; detalhe e pesquisa públicos não a mostram | `test_no_public_exposure_and_native_routes_cannot_edit_or_publish_offer` |
|
||||
| Modelo impede publicar ou retirar a marca privada; filtro público exclui estado incoerente introduzido directamente em QA | Mesmo teste e `test_model_cannot_publish_or_remove_private_phase_marker` |
|
||||
| Oportunidades anteriores não são convertidas pelo adaptador; âmbito nativo de divulgação mantém-se | `test_existing_opportunity_cannot_be_converted_through_offer_adapter` |
|
||||
| HTML privado usa entradas compiladas reais e resposta `private, no-store` | `test_full_html_uses_real_compiled_private_page_entries` e teste de recuperação |
|
||||
|
||||
São provas com actores e dados sintéticos na base isolada `offers_phase1_20261114_test:55441`. Não comprovam autenticação real observada num navegador, configuração de entrega de email, revisão humana de conteúdo ou funcionamento de NDA. Esses resultados não são alegados.
|
||||
|
||||
### Conferência visual e incidente da ferramenta
|
||||
|
||||
Foi tentada a observação, preservando as sessões existentes. O inventário da ferramenta identifica Edge e Firefox como aplicações, mas devolve `browsers: []` e o erro exacto `Browsers: Error: The browser access host is unavailable`. Não foi possível identificar uma aba nem ler o seu URL pelo conector.
|
||||
|
||||
Foi também tentado o acesso nativo de Windows através de Computer Use. A enumeração de aplicações respondeu, mas a captura da janela Edge falhou: `encode latest capture frame failed: copy Direct3D surface to SoftwareBitmap failed: A instância de dispositivo GPU foi suspensa. Utilize GetDeviceRemovedReason para determinar a ação apropriada. (0x887A0005)`.
|
||||
|
||||
Não foram repetidos reinícios, alteradas configurações ou desactivadas verificações. Não se observou a candidata e não se atribui a causa ao Edge ou ao portal. A conferência visual continua pendente por impedimento da ferramenta. Quando o acesso estiver disponível, conferir apenas a candidata: lista/criação, recuperação, preço «A negociar»/montante, contacto, estado após submissão, erros preservando os campos e apresentação estreita, sem voltar a executar a bateria técnica aceite.
|
||||
|
||||
### Os 61 ficheiros de formatação
|
||||
|
||||
`outputs/offers-private-phase1-20261114/closure-confirmation.json` contém a lista completa, o blob Git de cada ficheiro e a comparação individual. Resultado: **61/61 fora do delta; 61/61 idênticos à revisão base `e8408c27`; intersecção com a entrega vazia**. Os ficheiros de dependências também não integram o delta.
|
||||
|
||||
Não foram corrigidos ou reformatados nesta instrução. A falha global mantém-se registada, sem ser atribuída à Fase 1 nem declarada dispensada. Uma regularização desses ficheiros seria uma alteração separada; não é incluída por conveniência neste plano.
|
||||
|
||||
## 2. Plano técnico proposto para a Fase 2 — aguarda autorização
|
||||
|
||||
### Resultado e fronteira
|
||||
|
||||
Permitir que uma oferta submetida entre nas filas do Dashboard Colaborativo existente, seja revista, devolvida ou rejeitada com fundamento, corrigida e reenviada, ou aprovada internamente na sua versão exacta. Conservar decisões, responsáveis, tarefas, notificações e histórico. **A Fase 2 proposta termina na aprovação privada; a publicação pública permanece bloqueada para Ofertas.** A aprovação não certifica o negócio, a titularidade, o financiamento ou uma validação jurídica que não tenha ocorrido.
|
||||
|
||||
Sem novo dashboard, cadastro, autenticação ou motor editorial. Sem alterar negócios existentes, `mne`, menu principal ou regras dos perfis completos de Oportunidades. Não inclui catálogo público, fotografias, NDA, comprovativos de propriedade, validade, destaque comercial ou SMS novo. As decisões desses ramos não bloqueiam a revisão dos cinco campos comuns em privado.
|
||||
|
||||
### Arquitectura e persistência
|
||||
|
||||
1. Manter `opportunities.id`, `owner_user_id`, `organization_id`, `management_version`, `management_draft`, a marca `workflow_context.private_offer_preparation` e o histórico da Fase 1. A marca permanece e o filtro público continua a excluí-la.
|
||||
2. Acrescentar um adaptador delimitado ao percurso privado para fornecer validação comum, snapshot, hash e estado ao workflow nativo de Oportunidades. Reutilizar `OpportunityProfileWorkflow`, `PortalProfileAccess`, `OpportunityWorkflowTasks` e a integração de oportunidades em `PortalEditorialWorkflow`; não duplicar as transições num segundo motor.
|
||||
3. Não encaminhar simplesmente o formulário reduzido para `OpportunityCanonicalContract::validateSubmission`: esse contrato exige os perfis completos, evidências e verificações especializadas. Não fabricar esses campos nem enfraquecer as validações dos restantes investimentos. O adaptador distingue explicitamente a preparação comercial comum da validação integral do projecto.
|
||||
4. Separar estado editorial privado e disponibilidade pública. `working_state` pode representar a revisão; `status` mantém a barreira privada da oferta e `published_at` permanece nulo. O workflow existente actualmente escreve ambos: a implementação terá de delimitar esse comportamento às Ofertas, preservando-o nos restantes registos.
|
||||
5. Evitar estados divergentes entre `private_offer_preparation.state` e `working_state`: definir uma projecção única de estado para apresentação, autorização e filas, com compatibilidade de leitura da Fase 1. Não converter todos os registos por uma escrita em lote nem criar tarefas ao abrir uma página. A vinculação inicial de uma submissão anterior deve ser explícita, idempotente e auditada, conservando autor, data e revisão originais.
|
||||
6. Usar os registos existentes de `tasks`, `audit_events` e `portal_editorial_notifications`. Esta última já tem referência a oportunidade, versão e índice único por utilizador/oportunidade/versão/acção. Não se prevê uma nova tabela ou migração para o núcleo proposto. Se uma necessidade concreta de persistência não couber nos contratos existentes, apresentar o delta aditivo antes de o executar; não instalar estrutura por esta instrução.
|
||||
|
||||
### Estados e operações propostas
|
||||
|
||||
| Origem | Operação e responsável | Destino e garantia |
|
||||
| --- | --- | --- |
|
||||
| Rascunho | Promotor submete a versão conferida | Submetida; snapshot e tarefa de revisão vinculados à versão/ciclo |
|
||||
| Submetida | Validador autorizado inicia/recebe revisão | Em revisão; responsável explícito, sem atribuição inventada |
|
||||
| Submetida ou em revisão | Validador pede correcção, com fundamento e campos a rever | Correcções solicitadas; edição permitida apenas ao promotor autorizado |
|
||||
| Submetida ou em revisão | Validador rejeita, com fundamento obrigatório | Rejeitada; decisão e versão conservadas |
|
||||
| Correcções solicitadas ou rejeitada | Promotor escolhe «Corrigir e reenviar» | Nova preparação privada ligada à anterior; a rejeição não é apagada nem revertida em aprovação |
|
||||
| Nova preparação corrigida | Promotor guarda e submete | Novo ciclo de revisão; sem duplicação de oportunidade, tarefa ou notificação por repetição do pedido |
|
||||
| Em revisão | Validador independente aprova o snapshot actual | Aprovada internamente; nenhuma exposição pública |
|
||||
| Aprovada internamente | Promotor prepara alteração | Nova revisão privada; aprovação anterior histórica, sem aplicação à nova versão |
|
||||
| Qualquer estado de Ofertas nesta fase | Qualquer tentativa de publicar ou divulgar | Recusada no servidor, mesmo por Publicador ou através de rotas alternativas |
|
||||
|
||||
Reutilizar a regra nativa de fundamento de decisões (`reason`, 20–1000 caracteres), sem substituir fundamentação por um checkbox. Campos de correcção são referências aos dados da oferta, não exigências de um perfil especializado ainda por aprovar. A proposta de retoma após rejeição é própria das Ofertas; não alterar o recurso administrativo dos outros percursos.
|
||||
|
||||
### Autorizações e independência
|
||||
|
||||
| Actor existente | Âmbito proposto |
|
||||
| --- | --- |
|
||||
| Promotor | Criar/editar nos estados permitidos, submeter e preparar correcção, dentro das categorias e representação efectivas; nunca aprovar a sua oferta |
|
||||
| Validador | Consultar/rever/devolver/rejeitar/aprovar dentro da atribuição actual; decisões com MFA recente e verificação de conflitos |
|
||||
| Publicador | Não recebe aprovação por inferência e não publica Ofertas nesta fase; futura publicação continua operação e actor distintos da aprovação |
|
||||
| Auditor | Consulta autorizada e histórico nos limites existentes; sem decisões operacionais |
|
||||
| Gestor/Superutilizador | Conserva os controlos existentes; a condição administrativa não concede automaticamente acesso operacional, âmbito ou dispensa de independência |
|
||||
|
||||
Rever autorização no servidor em cada pedido, API, pré-visualização, tarefa e ligação de notificação; repetir dentro da transacção antes da decisão. Autor, contribuidores e representantes com conflito não aprovam a mesma oferta, mesmo acumulando papéis. Não estender a designação jurídica da legislação a Ofertas. Sem designação jurídica expressa e texto aprovado não se regista «validação jurídica» fictícia.
|
||||
|
||||
### Versões, auditoria e concorrência
|
||||
|
||||
Cada submissão congela um snapshot dos campos comuns, preço/modo/moeda, contacto referenciado, categoria, promotor e localização, com versão do contrato, revisão e hash determinístico. As referências de contacto e localização devem identificar também a versão/hash dos dados efectivamente conferidos; uma alteração posterior na fonte sinaliza reconferência, sem substituir silenciosamente a conclusão. Não criar outro cadastro de contactos ou territórios. Os recursos que futuramente entrem no âmbito terão identificador/versão/hash próprios; a aceitação deles não é antecipada nesta fase.
|
||||
|
||||
Decisões guardam actor, instante UTC, fundamento, transição, ciclo, versão e hash. Edição posterior exige novo ciclo e nova aprovação; a decisão anterior permanece consultável, associada ao snapshot anterior. Reutilizar snapshots cifrados da auditoria e restrições de leitura existentes; não copiar contactos ou documentos para registos públicos.
|
||||
|
||||
Usar `etag`, bloqueio da oportunidade e incremento de versão na mesma transacção. Decisão com versão desactualizada responde a conflito, sem sobrescrever o trabalho do promotor. Repetições da mesma operação não repetem decisões, tarefas ou notificações. Falha na operação reverte conjuntamente os seus efeitos de estado, tarefa e auditoria.
|
||||
|
||||
### Dashboard, tarefas e notificações
|
||||
|
||||
Integrar na secção de Oportunidades já acessível pelo Dashboard Colaborativo, com filtro para Ofertas e estados da revisão. Não criar outro item no menu principal. O controlador editorial actual lista artigos/legislação/documentos e liga Oportunidades separadamente; não assumir que acrescentar um tipo de conteúdo às tabelas editoriais resolve automaticamente esta integração.
|
||||
|
||||
As contagens devem considerar cada oportunidade uma vez: o contador actual já abrange rascunhos de Ofertas. Mostrar ofertas por rever sem duplicar o total entre uma nova vista filtrada e a vista geral. Tarefas sem responsável são visíveis na fila autorizada; não escolher um funcionário ou prazo por inferência. Reenvio cria novo ciclo e conserva tarefas/decisões encerradas.
|
||||
|
||||
Usar a caixa de notificações editorial existente, com ligações para o detalhe privado correcto e revalidação de acesso na listagem e no destino. Notificar autor e responsáveis efectivos sobre submissão, atribuição, correcção, rejeição, reenvio e aprovação, conforme o papel de cada destinatário; não enviar conteúdo confidencial a uma lista geral de gestores. O mecanismo actual de oportunidades cobre autor e responsáveis atribuídos: a escolha de coordenação inicial deve respeitar as designações existentes e o âmbito, deixando a fila por atribuir quando não houver destinatário elegível.
|
||||
|
||||
Para email, reutilizar o transporte e a fila existentes, após confirmação da transacção, com tentativa/erro rastreáveis e revalidação de destinatário. `NotifyInterestReceived` é de manifestações de interesse: reutiliza-se o padrão de transporte, não o seu texto ou evento como se fossem Ofertas. Não prometer entrega exactamente uma vez, leitura da mensagem ou SMS configurado. Em QA, manter mail fake/array e dados sintéticos; não enviar mensagens reais nesta preparação.
|
||||
|
||||
### Componentes existentes provavelmente afectados
|
||||
|
||||
| Componente | Alteração delimitada proposta |
|
||||
| --- | --- |
|
||||
| `app/Services/PrivateOfferPreparation.php` | Validação/snapshot comuns, compatibilidade da submissão F1 e preparação de correcção/reenvio |
|
||||
| `app/Services/OpportunityProfileWorkflow.php` | Adaptar revisão, decisões, ciclos e persistência privada apenas quando a oportunidade for Oferta |
|
||||
| `app/Services/PortalProfileAccess.php` | Estado único e operações/edição permitidas nos estados de correcção, conservando âmbito e conflito |
|
||||
| `app/Services/OpportunityManagement.php` | Preservar versões e invalidação de aprovação da revisão alterada, sem converter registos anteriores |
|
||||
| `app/Services/OpportunityWorkflowTasks.php` | Reutilizar vinculação idempotente por oportunidade/revisão/ciclo e tarefas anteriores |
|
||||
| `app/Services/PortalEditorialWorkflow.php` | Filas/contagens, destinatários e ligações das notificações de Ofertas, sem duplicar o cadastro editorial |
|
||||
| `app/Http/Controllers/PortalEditorialWorkflowController.php` e `OpportunityManagementController.php` | Vista filtrada e dados autorizados de revisão, reaproveitando a entrada de Oportunidades |
|
||||
| `app/Http/Controllers/PrivateOfferPreparationController.php` e `PortalProfileController.php` | Pedidos privados e decisões encaminhados ao serviço autorizado; sem lógica de permissão apenas no cliente |
|
||||
| `app/Http/Middleware/ProtectProfileOpportunity.php` | Permitir somente as operações F2 expressamente suportadas; manter recusados os restantes atalhos |
|
||||
| `app/Models/Opportunity.php` e `app/Services/OpportunityPromotion.php` | Conservar exclusão pública, marca privada e recusa de divulgação; não remover as barreiras ao habilitar revisão |
|
||||
| `resources/js/pages/offers/preparation.tsx`, `preparation-index.tsx` e `components/opportunity-profile-actions.tsx` | Estado, correcção, reenvio, fundamento e acções de cada papel, com campos preservados em conflito |
|
||||
| `resources/js/pages/management/editorial-workflow.tsx` e `management/opportunities.tsx` | Reutilizar secção existente e filtros, sem reorganizar o menu |
|
||||
| `routes/web.php` | Só as rotas privadas necessárias, com middleware de autenticação, perfis e resposta privada |
|
||||
| Testes e os mesmos manual/matriz | Provas focadas, saldo de RF-MN-003 e distinção QA/instalado/pendente |
|
||||
|
||||
Os caminhos acima existem e foram inspeccionados. Não são uma autorização para modificar todos os ficheiros: usar apenas os que o delta demonstrar necessários. Novos auxiliares ou testes deverão ter nomes definidos na implementação, sem serem apresentados aqui como componentes já existentes.
|
||||
|
||||
### Ordem de implementação proposta e aceitação
|
||||
|
||||
1. **Estado e versão:** adaptar a submissão F1 sem perder os seus identificadores/datas; congelar snapshot, ciclo e permissões. Demonstrar que uma Oferta e uma oportunidade normal mantêm os respectivos contratos.
|
||||
2. **Revisão privada:** integrar tarefas e fila existente; iniciar, devolver, rejeitar e aprovar com fundamento, MFA, âmbito e independência. Aprovação continua sem publicação.
|
||||
3. **Correcção e reenvio:** demonstrar rejeição → nova revisão privada → guardar/recuperar → novo ciclo → aprovação, conservando rejeição, comentários, snapshots e tarefas antigos.
|
||||
4. **Notificações:** emitir uma ocorrência por evento/destinatário na caixa existente; testar repetição, falha/rollback, permissão revogada e URL de destino. Email real não é condição para aprovar o transporte fake de QA nem é activado por ele.
|
||||
5. **Aceitação:** testes HTTP de sucesso, erro e concorrência, autorização por organização/categoria/estado, perda de representação, auditor e acumulação de papéis, motivo ausente, hash desactualizado, reenvio sem duplicações e nenhuma exposição por catálogo/API/rotas de divulgação. Regressão focada dos percursos nativos afectados, tipos, análise PHP, lint e compilação conforme o executor existente. Conferência visual quando a ferramenta permitir.
|
||||
|
||||
Percurso de aceitação principal: promotor prepara/submete → validador consulta na fila → rejeita com fundamento → promotor corrige e reenvia a nova revisão → validador independente aprova → autor recupera decisão e histórico → tentativa de publicação permanece recusada.
|
||||
|
||||
### Decisões para a autorização da Fase 2
|
||||
|
||||
Solicitar aprovação do recorte acima, incluindo: aprovação exclusivamente interna; correcção/reenvio após rejeição como nova revisão; estado privado compatível com os dados F1; integração pela área existente de Oportunidades no Dashboard; notificações internas e email apenas simulado em QA. Não são necessárias novas contas ou permissões por defeito.
|
||||
|
||||
Uma designação operacional ausente bloqueia apenas a atribuição a essa pessoa; não justifica inventar destinatários nem suspender as restantes filas autorizadas. NDA, comprovativos, textos jurídicos, formatos/tamanhos das seis fotografias e validade continuam nas fases dependentes. A regra de separação aprovação/publicação mantém-se; publicação, instalação e qualquer alteração operacional exigem autorização expressa própria.
|
||||
|
||||
### Preservação e reversão da futura preparação QA
|
||||
|
||||
Preparar o delta F2 num commit/pacote próprio, após a sua autorização, conservando a referência F1. A revisão técnica deve mostrar alterações apenas nos componentes afectados e revalidar as barreiras de exposição. Uma reversão de código ou desactivação do percurso F2 deve deixar as ofertas e decisões existentes legíveis em privado e bloqueadas para publicação; não eliminar eventos, tarefas, notificações ou snapshots para regressar à versão anterior. Não executar rollback destrutivo de estrutura com histórico.
|
||||
|
||||
Qualquer futura entrega em homologação terá a sua autorização, conferência do delta e processo Git de backup/restauro e recuperação. Este plano não executa nem autoriza essas operações.
|
||||
|
||||
**Estado final desta instrução:** relatório e plano entregues para decisão. Nenhuma implementação F2, migração, instalação, publicação, envio de notificações ou modificação da implementação F1.
|
||||
@@ -0,0 +1,75 @@
|
||||
# Ofertas — Fase 2, circuito editorial privado em QA
|
||||
|
||||
14/11/2026. Autorização expressa da Tempo nesta sessão, limitada à Fase 2 do plano `6e302db6`. Base: Fase 1 `1f2990e4`, mantida no histórico Git. Referência: RF-MN-003 da matriz existente.
|
||||
|
||||
**Implementado e verificado tecnicamente em QA. Sem instalação em homologação ou produção. A aprovação é interna; a publicação de Ofertas permanece bloqueada. Fase 3 não iniciada.**
|
||||
|
||||
## Percurso que ficou utilizável
|
||||
|
||||
No Dashboard Colaborativo existente, a área de Oportunidades inclui a entrada «Ofertas — revisão privada». Abre a mesma lista privada da Fase 1, agora com filtros por estado. Não há outro dashboard, outra tabela de ofertas, outro login ou alteração ao menu principal. As contagens de Oportunidades continuam a contar cada registo uma vez; a entrada filtrada não acrescenta outro contador ao total.
|
||||
|
||||
O promotor cria/guarda/recupera/submete. Um validador autorizado inicia a revisão, pede correcções nos campos comuns ou rejeita com fundamento. O promotor prepara a revisão após rejeição ou aprovação, conserva a decisão anterior, corrige e reenvia no mesmo identificador. O validador independente aprova a nova versão em privado. A coordenação de validação existente pode atribuir/reassociar um revisor elegível, com fundamento, histórico do responsável anterior e do novo responsável; não são criadas designações ou atribuições por inferência.
|
||||
|
||||
Estados: rascunho → submetida → em revisão → correcções solicitadas/rejeitada/aprovada internamente. Correcção e reenvio abrem novo ciclo; não eliminam rejeições, aprovações, snapshots ou tarefas encerradas. Aprovação não publica nem certifica titularidade, licenciamento ou viabilidade do negócio.
|
||||
|
||||
## Integração e garantias
|
||||
|
||||
- O mesmo `OpportunityProfileWorkflow` executa as transições. O adaptador privado valida os campos comerciais comuns; não exige nem fabrica os perfis normativos completos e não enfraquece o contrato dos investimentos existentes.
|
||||
- Oportunidades continua a ser o cadastro principal. `management_draft`, versões, etag, workflow_context, tarefas CRM, auditoria e notificações existentes são reutilizados. Não há novas migrações, tabelas ou mudanças em `mne`.
|
||||
- O estado editorial da Oferta é projectado a partir de uma única origem privada e sincronizado com as filas. O estado de publicação conserva a barreira privada: `status=draft`, `published_at=null` e a marca de preparação permanece. Nenhuma operação F2 remove a exclusão do catálogo.
|
||||
- Cada submissão regista versão do contrato, snapshot, hash determinístico, versão do registo submetido e ciclo de revisão. Cada decisão conserva actor, instante UTC, fundamento, hash e revisão a que respeita. O snapshot da decisão também fica cifrado na auditoria existente.
|
||||
- Contacto e localização são referenciados pelos cadastros existentes. O hash inclui os dados efectivamente conferidos nas referências; uma mudança posterior impede a aprovação dessa submissão ou sinaliza reconferência se a aprovação já ocorreu. A leitura não substitui silenciosamente a decisão.
|
||||
- Ofertas antigas da Fase 1 só são vinculadas na primeira operação explícita de revisão. Conservam autor, data e revisão originais. A vinculação regista quem/quando estabeleceu o fingerprint: a Fase 1 não tinha esse fingerprint de referências, pelo que não se inventa um estado histórico do CRM na data original.
|
||||
- As permissões são verificadas no servidor, na transacção e nos destinos das notificações. Autor, contribuidores e representantes em conflito não aprovam; acumulação de papéis não remove os bloqueios. Revisão/decisões internas exigem MFA recente. O papel de auditor permanece sem escrita operacional.
|
||||
- Tentativas de publicação são recusadas na rota privada, nas rotas alternativas nativas e no workflow chamado directamente. Os bloqueios anteriores do modelo e da divulgação e a exclusão da pesquisa pública permanecem. Não se implementou um publicador para Ofertas.
|
||||
- A versão pública anterior dos percursos nativos mantém-se durante edições pendentes; o ramo nativo não foi convertido para a preparação comercial. A regressão dos registos publicados confirma a preservação dos valores públicos. Não existem Ofertas públicas aceites nesta fase para simular uma publicação nova.
|
||||
|
||||
## Notificações e recuperação
|
||||
|
||||
A caixa editorial existente recebe as ocorrências por oportunidade/versão/acção e destinatário. A restrição de unicidade existente evita ocorrências repetidas. Submissões notificam coordenação elegível já designada; sem designação, a fila fica por atribuir. Decisões notificam autor e responsáveis efectivos, e a reatribuição inclui o responsável anterior se continuar autorizado. As ligações abrem a Oferta privada correcta e revalidam o acesso.
|
||||
|
||||
O novo job é apenas o consumidor do transporte de email/fila já existente: usa a activação e a lista de destinatários operacionais actuais, consulta a notificação novamente, reconfirma acesso e não envia conteúdo da Oferta. É agendado após a confirmação da transacção. Falhas imediatas de despacho/entrega ficam auditadas sem converter a decisão já confirmada numa resposta incerta. O job usa as tentativas/backoff do padrão existente. Não existe transporte SMS novo, envio real em QA ou promessa de entrega exactamente uma vez.
|
||||
|
||||
Se o circuito editorial ficar indisponível, uma Oferta v2 não é submetida pelo fallback v1: responde com indisponibilidade e conserva versão e histórico. Ofertas genuinamente v1 continuam com a preparação anterior. Não se faz rollback destrutivo de dados para regressar ao código precedente.
|
||||
|
||||
## Ficheiros do delta
|
||||
|
||||
| Ficheiros | Finalidade |
|
||||
| --- | --- |
|
||||
| `app/Services/PrivateOfferPreparation.php` | Adaptador, snapshots, vinculação F1, apresentação e histórico privado |
|
||||
| `app/Services/OpportunityProfileWorkflow.php` | Revisão, atribuição, decisões por versão e ciclos no motor existente |
|
||||
| `app/Services/PortalProfileAccess.php` | Projecção do estado e edição nos estados permitidos |
|
||||
| `app/Services/OpportunityManagement.php` | Guardar a correcção privada sem perder categoria/âmbito/localização actuais |
|
||||
| `app/Services/PortalEditorialWorkflow.php` | Destinatários, unicidade, destino privado e agendamento de notificações |
|
||||
| `app/Jobs/NotifyPrivateOfferDecision.php` | Email pelo transporte existente, com revalidação de acesso |
|
||||
| `app/Http/Controllers/PrivateOfferPreparationController.php` | Filtros e endpoint privado de decisões |
|
||||
| `app/Http/Controllers/PortalEditorialWorkflowController.php` | Entrada de Ofertas na área existente de Oportunidades |
|
||||
| `routes/web.php` | Endpoint privado de revisão, sob autenticação/perfis/resposta privada |
|
||||
| `resources/js/components/offer-editorial-review.tsx` | Fundamento, campos a corrigir, responsável, decisões e histórico |
|
||||
| `resources/js/lib/offer-editorial.ts` | Tipos e rótulos partilhados, sem importar uma página como biblioteca |
|
||||
| `resources/js/pages/offers/preparation.tsx` e `preparation-index.tsx` | Revisão, recuperação e filtros na preparação existente |
|
||||
| `resources/js/pages/management/editorial-workflow.tsx` | Entrada integrada, sem duplicar contagens |
|
||||
| `tests/Feature/PrivateOfferEditorialTest.php` | 11 testes novos do circuito e dos bloqueios |
|
||||
| Matriz, manual, este relatório e recibo QA | Saldo e evidência da entrega, sem outro inventário funcional |
|
||||
|
||||
## Verificação
|
||||
|
||||
Base sintética isolada: `offers_phase1_20261114_test:55441`, com as guardas existentes. Não foram utilizados processos reais, contas operacionais ou notificações externas.
|
||||
|
||||
- Integração e regressão: **35 testes / 499 asserções aprovados**, incluindo Fase 1, permissões, gravação/publicação nativa e o cenário canónico/editorial afectado.
|
||||
- Passagem final F2: **11 testes / 160 asserções aprovados**. O teste de conflito de contribuidores foi ainda reforçado e verificado separadamente: **1 teste / 14 asserções aprovado**.
|
||||
- HTML completo das duas páginas privadas com o build final: **1 teste / 10 asserções aprovado**.
|
||||
- Pint, PHPStan, tipos, lint/formatação focados e compilação QA aprovados. A análise PHP foi repetida para o motor após o ajuste final do ramo privado. Sem enfraquecer testes, baseline ou guardas.
|
||||
- Os grupos sobrepõem-se; não somar os totais. Execuções interrompidas ou anteriores com falhas não são contadas como aprovadas.
|
||||
|
||||
Os testes cobrem fila existente sem contagem duplicada, rejeição/correcção/reenvio, aprovação vinculada ao snapshot, decisão desactualizada, âmbito/MFA/contribuidores/auto-aprovação, perda de acesso, contacto alterado, reatribuição restrita, rollback conjunto, notificação repetida, email no transporte local em memória, revogação antes da entrega, vinculação explícita F1, indisponibilidade sem downgrade e recusa de publicação por percursos alternativos. O histórico recuperado conserva o título/preço e a decisão de cada versão.
|
||||
|
||||
## Riscos e pendências
|
||||
|
||||
1. Conferência visual manual continua pendente: a última prova da ferramenta registou host de navegador indisponível e falha de captura nativa GPU `0x887A0005`. Não foram repetidos reinícios ou desactivadas verificações. HTML e compilação não são apresentados como aceite visual.
|
||||
2. A verificação global de formatação continua a falhar nos 61 ficheiros preexistentes. A lista foi novamente confrontada com este delta, sem intersecção. Não foi reformatado conteúdo exterior ao âmbito nem dispensada a falha global.
|
||||
3. Email real, configuração operacional da fila, destinatários e entregabilidade não foram activados nem demonstrados. Os testes utilizam transporte em memória e os papéis sintéticos existentes no cenário.
|
||||
4. Atribuição/reassociação exige coordenação já designada e candidatos elegíveis. A inexistência de uma designação não autoriza criar papéis; bloqueia apenas essa atribuição e mantém a fila por tratar.
|
||||
5. O recorte aprovado é moderação dos campos comerciais comuns. NDA, comprovativos, fotografias, validade, catálogo público, destaque e restantes fases não são declarados concluídos. Não se atribui a Ofertas a designação jurídica da legislação.
|
||||
|
||||
**Saldo:** Fase 2 implementada e verificada tecnicamente em QA; não instalada. O módulo integral/RF-MN-003 e Procuras continuam parciais. A Fase 3 e qualquer instalação exigem autorização expressa separada. Próximo passo: revisão desta entrega e conferência visual quando o ambiente permitir, sem reabrir as provas técnicas aceites.
|
||||
@@ -0,0 +1,56 @@
|
||||
# Ofertas — Fase 3: consulta e filtros em QA
|
||||
|
||||
14/11/2026. Desenvolvimento autorizado exclusivamente para a Fase 3. Base aceite: `91c18573` (Fase 2), preservando `1f2990e4` (Fase 1) e o plano `6e302db6`. Referência: RF-MN-003 da matriz existente.
|
||||
|
||||
**Consulta implementada em QA; publicação efectiva bloqueada. Sem instalação em homologação ou produção. Conferência visual/móvel pendente. Não foram iniciadas as Fases 4 ou 5.**
|
||||
|
||||
## Comportamento implementado
|
||||
|
||||
`/ofertas` reutiliza os filtros, paginação, estados de pesquisa, estrutura pública e mapa do catálogo existente. Apresenta grelha/lista e lista com mapa administrativo. `/ofertas/{id}` mostra o resumo comercial, categoria, localização geral, preço e data da versão publicada. Não houve alteração do menu nem criação de outro cadastro ou dashboard.
|
||||
|
||||
Filtros: texto do resumo público, categoria, província/distrito, montante ou preço a negociar, moeda e intervalo de preços. A hierarquia territorial é validada no servidor. Montantes são comparados como decimais exactos, incluindo valores grandes; ordenar ou filtrar por montante exige uma moeda. MZN e USD são as moedas actualmente suportadas na preparação de Ofertas. Não há conversão cambial. Os contratos de moeda/preço dos investimentos nativos mantêm-se.
|
||||
|
||||
A API de leitura tem `/api/v1/offers`, `/offers/{id}`, `/offers/map` e `/offers/export`. A exportação CSV usa a mesma projecção e filtros da lista, com protecção contra fórmulas em células. Não existe operação HTTP de publicação neste consumidor.
|
||||
|
||||
## Barreira de confidencialidade e limite desta entrega
|
||||
|
||||
Esta fase não autoriza divulgar as Ofertas privadas já existentes. O modelo, os workflows e as rotas alternativas conservam os bloqueios de publicação aceites na Fase 2. O catálogo e os detalhes novos continuam vazios/404 na configuração normal, mesmo que exista JSON com um alegado estado publicado.
|
||||
|
||||
O novo serviço só admite amostras sintéticas quando é explicitamente injectado pelo teste e a guarda existente confirma ambiente `testing`, ligação de loopback, porta `55441`, base terminada em `_test` e identidade PostgreSQL correspondente. Não há interruptor HTTP, variável de ambiente ou sinalizador em dados que active a consulta efectiva. O mesmo modo injectado fora desse ambiente é recusado.
|
||||
|
||||
O consumidor lê exclusivamente uma projecção pública independente, com contrato, revisão, resumo autorizado, hash e decisões distintas de aprovação e publicação. O hash tem de corresponder ao conteúdo exacto; actores iguais, estados incompletos, datas futuras, referências territoriais inacessíveis e propriedades de conteúdo não previstas invalidam a projecção. Os testes constroem estas amostras: não representam decisões reais nem publicações realizadas.
|
||||
|
||||
Contactos, anexos, fotografias, dados financeiros privados, localização exacta, rascunho e contexto editorial completo não são seleccionados nem serializados. Os mapas recebem apenas limites administrativos públicos; nunca marcadores, coordenadas do negócio ou geometrias privadas. Texto é apresentado como texto React, sem HTML introduzido pelo conteúdo.
|
||||
|
||||
Uma edição privada ou rejeição da nova revisão não substitui a projecção anterior aprovada/publicada. Filtros, detalhe e CSV continuam a usar essa projecção; uma Oferta sem publicação válida não aparece. Eliminação lógica e retirada de uma referência territorial pública impedem a leitura.
|
||||
|
||||
**A futura criação/autorização operacional dessa projecção, a classificação dos campos divulgáveis e o levantamento da barreira dependem dos mecanismos de confidencialidade aplicáveis e de autorização própria. Não foram implementados NDA nem os restantes percursos das Fases 4/5. Não declarar a publicação real concluída.**
|
||||
|
||||
## Ficheiros alterados
|
||||
|
||||
| Grupo | Finalidade |
|
||||
| --- | --- |
|
||||
| `app/Services/OfferPublicCatalogue.php` | Consumidor de projecções, guardas, filtros, ordenação decimal, paginação e mapas administrativos |
|
||||
| `app/Http/Controllers/OfferPublicCatalogueController.php`, `routes/web.php`, `routes/api.php` | Páginas públicas e endpoints de leitura/CSV |
|
||||
| `app/Services/InvestmentCatalogue.php` | Opção delimitada para preços decimais de Ofertas; comportamento nativo por defeito preservado |
|
||||
| `resources/js/app.tsx`, `components/opportunity-search-filters.tsx` | Layout público e reutilização dos filtros existentes |
|
||||
| `resources/js/pages/offers/catalogue.tsx`, `public-detail.tsx`, `components/public-offer-card.tsx`, `lib/public-offer.ts` | Consulta, detalhe e apresentação exacta do preço |
|
||||
| `tests/Feature/OfferPublicCatalogueTest.php` | Isolamento, projecções, acesso directo, versões, filtros, mapas, CSV e HTML compilado |
|
||||
| Matriz RF-MN-003 e manual existentes | Saldo actualizado, sem outro inventário |
|
||||
|
||||
Sem migrações, dependências instaladas, modificações em `mne`, permissões novas ou alterações de dados operacionais. Testes usam apenas dados sintéticos e rollback na base QA isolada.
|
||||
|
||||
## Verificação e pendências
|
||||
|
||||
O recibo `outputs/offers-public-phase3-20261114/qa-result.json` regista os resultados finais e os hashes do delta. Os grupos de testes sobrepostos não devem ser somados.
|
||||
|
||||
- Passagem final: **10 testes / 133 asserções aprovados**, incluindo a regressão dos filtros combinados, lista e exportação nativos com referências sintéticas autocontidas, sem alterar o ensaio anterior.
|
||||
- Passagem de integração anterior: **41 testes, 36 aprovados e cinco impedidos**, 514 asserções. As Fases 1/2 e a gestão/preservação pública nativa passaram. Os cinco casos de `OpportunitySearchTest` exigem uma referência pública preexistente de Inhambane e não chegam às respectivas asserções nesta base vazia. A limitação foi registada, sem remover ou enfraquecer os casos; a nova prova autocontida cobre o contrato partilhado afectado.
|
||||
- Pint, PHPStan sem erros, TypeScript, lint/formatação dos seis ficheiros frontend alterados e compilação QA aprovados. HTML real das duas páginas novas resolve os respectivos ficheiros compilados.
|
||||
- A verificação global continua a acusar os **61 ficheiros preexistentes**, fora do delta; não foram reformatados. Permanecem os avisos não impeditivos do build sobre a dependência opcional `fontaine` e o tamanho de chunks.
|
||||
|
||||
A estrutura responsiva foi preparada com campos em uma coluna no ecrã estreito, grelha progressiva, quebra de texto e controlos que se ajustam à largura. **Não há aceite visual ou prova de apresentação móvel:** a tentativa desta fase devolveu `Browsers: Error: The browser access host is unavailable`. O Playwright do caminho utilizado pelo antigo ensaio do repositório também não está disponível. Não foram instaladas ferramentas, reiniciados navegadores ou desactivados controlos para contornar a limitação. Compilação e respostas HTML não substituem essa prova.
|
||||
|
||||
O consumidor limita a representação a 1.000 projecções candidatas; o CSV e a página assinalam resultados truncados. O mapa limita o contexto a 30 territórios. Estes limites não são uma demonstração de escala de produção e devem ser revistos antes de uma abertura efectiva com volume superior.
|
||||
|
||||
Mantêm-se a conferência visual e a entrega real de email da Fase 2. A Fase 3 não altera o transporte de email. O módulo integral e Procuras não estão concluídos. Próxima tarefa: conferir desktop/móvel quando o conector estiver disponível e submeter esta entrega QA à apreciação da Tempo, mantendo a publicação bloqueada e sem instalação.
|
||||
@@ -0,0 +1,88 @@
|
||||
# Ofertas — Fase 4: particularidades e confidencialidade em QA
|
||||
|
||||
14/11/2026. Base aceite `7691cab3`, preservando F1 `1f2990e4`, F2 `91c18573` e o plano `6e302db6`. Desenvolvimento exclusivo em QA autorizado pela Tempo. Os campos propostos no documento original foram confirmados nesta sessão como opcionais durante a preparação. A Tempo confirmou também que o NDA permanece pendente.
|
||||
|
||||
**Implementado em QA, com dependência documental para o NDA real. Publicação efectiva bloqueada. Sem instalação, alterações de dados operacionais, migrações ou modificações em `mne`. Fase 5 não iniciada.**
|
||||
|
||||
## Percursos implementados
|
||||
|
||||
Na mesma preparação privada de Ofertas, o promotor guarda, recupera e altera:
|
||||
|
||||
- **Venda de empresa:** sectores principal/secundário do cadastro existente; receita, lucro/fluxo de caixa, moeda independente, motivo da venda e disponibilidade declarada. Decimais são conservados como texto, incluindo fluxo de caixa negativo; não há conversão cambial. Estes dados não entram na projecção pública.
|
||||
- **Imóvel:** venda/arrendamento, tipo do catálogo existente, áreas e unidade, quartos/casas de banho, características/comodidades e disponibilidade declarada. Os campos de quartos e casas de banho aparecem nos tipos habitacionais aplicáveis. A localização continua no selector existente. As categorias de direitos de terra e restantes produtos conservam os seus contratos; não se fundiu terreno com imóvel nem se inventou uma regra de titularidade.
|
||||
- **Fotografias:** envio, legenda, declaração e descrição de edição digital, alteração de metadados/original, retirada e restauro lógico. Máximo de **seis fotografias activas**, verificado no servidor sob bloqueio da mesma Oferta, tanto na criação como no restauro. Os recursos nativos dos outros módulos mantêm o limite anterior.
|
||||
- **Comprovativos imobiliários:** envio privado, estado de análise e consulta pelo validador interno autorizado. O anunciante recebe o estado do envio, mas não recebe uma ligação de leitura do comprovativo no editor. O suporte enviado não é apresentado como título certificado nem aprova o negócio.
|
||||
|
||||
Os ficheiros usam `OpportunityResources`, `documents`, ligações e histórico existentes, sem outro armazenamento documental. Originais são cifrados no disco privado `local`, com identificadores gerados pelo servidor, checksum e quarentena. Reutilizam-se validação MIME/conteúdo, limites de 10 MB para fotografia/20 MB para documento, mínimo de 400 × 400 píxeis e rejeição de imagens animadas/executáveis. Os formatos documentais são limites técnicos do serviço existente; não constituem uma lista jurídica de títulos admitidos. O scanner existente decide a disponibilidade. Nenhum envio passa automaticamente a limpo ou a publicado.
|
||||
|
||||
## Comprovativos e caminhos alternativos
|
||||
|
||||
A autorização comum de binários (`VerifiedDocumentBinary`) verifica também os comprovativos de Ofertas. Exige papel existente de validador com âmbito no registo, acesso efectivo, MFA recente e ausência de conflito; o promotor, auditor, outra categoria ou sessão sem MFA não obtêm os bytes. A verificação ocorre também nos consumidores gerais que reutilizam esse serviço, antes de qualquer entrega. Regista documento, hash, actor e versão da Oferta, sem incluir o original na auditoria.
|
||||
|
||||
As rotas nativas de edição de Ofertas que já estavam bloqueadas mantêm a recusa. A associação de originais públicos da biblioteca a uma Oferta é recusada neste percurso: um comprovativo reservado não é convertido em documento público por essa associação. Fotografias e localizações desta preparação só admitem visibilidade interna. Os comprovativos só admitem classificação restrita.
|
||||
|
||||
## Duas condições independentes para dados da empresa
|
||||
|
||||
O novo adaptador usa a mesma Oportunidade e a autorização existente. Não cria cadastro de empresas, utilizadores, permissões ou dashboard. A configuração `private_offers.nda` está **nula**; não contém texto jurídico nem exemplar fictício. A sua futura referência exige um documento existente com versão, SHA256 e referência da aprovação da Tempo. Estado, scan, versão e integridade dos bytes são reconferidos pelo serviço comum. Não se reutilizou a designação jurídica da legislação como aprovação automática de NDA.
|
||||
|
||||
Com um exemplar efectivamente aprovado, o percurso técnico é:
|
||||
|
||||
1. Interessado autenticado e com email verificado consulta o exemplar exacto e aceita-o expressamente para o hash da versão aprovada da empresa. Não é exigida uma justificação para aceitar ou retirar a aceitação.
|
||||
2. O vendedor registado (dono da Oferta), com atribuição de promotor e representação ainda efectivas, autoriza expressamente aquele interessado e **aquela aceitação**. Esta autorização no portal não certifica titularidade sobre a empresa. Um validador não substitui o vendedor. Autorizações e revogações conservam fundamento.
|
||||
3. Em cada consulta, o servidor reconfirma as duas condições, o exemplar/versão do NDA, a aprovação editorial actual, o hash dos dados, a identidade do vendedor, os direitos efectivos e a revisão de acesso do interessado.
|
||||
4. O interessado recebe apenas os campos empresariais da submissão aprovada. Não recebe a descrição privada integral, contactos, morada exacta, comprovativos, caminhos de armazenamento ou ficheiros originais. As fotografias continuam no percurso privado do promotor e da equipa; não se acrescentou partilha externa de originais.
|
||||
5. Revogar a autorização ou retirar a aceitação fecha o acesso seguinte. A retirada de uma aceitação anterior continua disponível enquanto a Oferta está em revisão, sem abrir os dados dessa revisão. Uma aceitação nova elimina a autorização anterior: exige nova decisão do vendedor. Mudar o NDA, a Oferta, os seus suportes, as referências abrangidas ou os direitos efectivos impede a reutilização silenciosa do acesso anterior.
|
||||
|
||||
Aceitações, autorizações, retiradas e revogações ficam na história da Oportunidade, com antes/depois, actor, instante, hash da Oferta e identidade/versão/checksum do NDA. Os fundamentos ficam cifrados. A auditoria existente regista operações e consultas autorizadas/recusadas, sem valores financeiros. O vendedor consulta as suas decisões; o interessado vê apenas as ocorrências que lhe respeitam. As respostas privadas mantêm `no-store`. Uma revogação não apaga informação que o interessado já tenha legitimamente recebido; não foi prometida recolha retroactiva de bytes.
|
||||
|
||||
## Continuidade editorial e protecção pública
|
||||
|
||||
Os campos específicos e os suportes activos são incorporados no snapshot editorial `private-offer-editorial-v3`. Hashes incluem identidades e versões dos recursos, conteúdo/metadados e checksums; alterações exigem nova submissão/decisão. Ofertas anteriores sem extensão mantêm o contrato v2. Identificadores, originais, rejeições, aprovações e histórico não são substituídos. Um suporte que volte a quarentena sinaliza aprovação desactualizada, conservando a possibilidade de recuperar e preparar nova revisão.
|
||||
|
||||
A projecção pública da Fase 3 e os bloqueios do modelo/workflow não foram abertos. Não existe publicador, pagamentos ou destaque comercial activo. Não se acrescentaram campos sensíveis a pesquisas, APIs públicas, mapas, CSV, notificações ou email. As provas anteriores da Fase 2 para notificações sem conteúdo privado são reutilizadas; a entrega real de email permanece pendente.
|
||||
|
||||
## Cinco testes territoriais antes bloqueados
|
||||
|
||||
A causa estava em `OpportunitySearchFixture`: exigia uma referência pública de Inhambane e o sector turismo previamente importados, apesar de o ensaio usar uma base de testes vazia. O produto não perdeu os dados territoriais. O fixture passa a criar os mínimos sintéticos apenas quando ausentes, **depois da guarda de ambiente testing/loopback/base `_test`/porta 55441 e dentro de uma transacção de rollback**. Fora dessa combinação recusa a criação. Não foi executada importação, reposição ou alteração no cadastro operacional.
|
||||
|
||||
Os seis casos de `OpportunitySearchTest`, incluindo os cinco impedidos, passaram. Não se alteraram as suas asserções ou os filtros do produto para obter aprovação.
|
||||
|
||||
## Ficheiros alterados
|
||||
|
||||
| Grupo | Finalidade |
|
||||
| --- | --- |
|
||||
| `PrivateOfferDetails`, `PrivateOfferPreparation` | Campos específicos e snapshots/versionamento existentes |
|
||||
| `PrivateOfferConfidentiality`, `config/private_offers.php` | Duas condições, revogação, versões e auditoria; NDA inactivo |
|
||||
| `OpportunityResources`, `OpportunityLibrarySupport`, `VerifiedDocumentBinary` | Limite de seis, metadados, visibilidade e protecção central dos comprovativos |
|
||||
| `PrivateOfferPreparationController`, `routes/web.php` | Operações privadas no percurso existente |
|
||||
| `offer-particulars`, `offer-private-resources`, `offer-confidential-access`, páginas `offers/preparation` e `offers/confidentiality` | Preparação progressiva, recursos e controlo privado de acesso |
|
||||
| `PrivateOfferConfidentialityTest`, `OpportunitySearchFixture` | Segurança/isolamento e independência das importações operacionais |
|
||||
| Matriz RF-MN-003, manual e recibo existentes | Estado, testes e dependências concretas |
|
||||
|
||||
## Verificação e saldo
|
||||
|
||||
Resultados finais, comandos, base isolada e hashes do delta: `outputs/offers-private-phase4-20261114/qa-result.json`. Grupos de testes sobrepostos não devem ser somados. A passagem alargada intermédia teve 48/50 aprovados; uma falha expôs uma referência de sessão desactualizada no cenário de revogação e a outra resultou de reconstruir assets durante a prova HTML. Foram corrigidas as causas e as verificações afectadas foram repetidas com o build estabilizado. Não se enfraqueceram asserções, permissões, scanner ou guardas.
|
||||
|
||||
| Verificação | Resultado |
|
||||
| --- | --- |
|
||||
| Passagem F4 e HTML das fases afectadas | 13 testes / 174 asserções aprovados |
|
||||
| Delta final: retirada durante revisão, versões NDA e revogação de sessão | 1 teste / 35 asserções aprovados |
|
||||
| HTML privado, público e de confidencialidade com o último build estabilizado | 3 testes / 29 asserções aprovados |
|
||||
| Regressão da preparação privada F1 | 14 testes / 146 asserções aprovados |
|
||||
| Regressão editorial F2 | 11 testes / 161 asserções aprovados |
|
||||
| Pesquisa territorial, incluindo os cinco casos antes impedidos | 6 testes / 112 asserções aprovados |
|
||||
| Recursos nativos: binários, uploads, autorização e processamento condicionado | 5 testes / 69 asserções aprovados |
|
||||
| Pint, PHPStan, TypeScript, lint/formatação focados e build QA | Aprovados; PHPStan sem erros |
|
||||
| Formatação global | Os mesmos 61 ficheiros, fora do delta e com conteúdo Git inalterado |
|
||||
|
||||
As regressões das Fases 1/2 e da pesquisa foram extraídas da passagem alargada; os cinco casos de recursos constam da passagem focada de 16 testes/217 asserções. O complemento final cobre a alteração de retirada sem repetir os restantes casos aprovados. O caso adicional de suporte novamente em quarentena teve a expectativa ajustada ao contrato **já existente** de reabertura (`needs_information`, editável), preservando o produto.
|
||||
|
||||
**Pendências concretas:**
|
||||
|
||||
- Exemplar, versão e referência da aprovação do NDA da Tempo. A ausência foi confirmada pelo proprietário; sem estes elementos o acesso real continua fechado. Os testes usam bytes explicitamente marcados como sintéticos e não jurídicos, por injecção local ao teste.
|
||||
- Identificação jurídica dos comprovativos imobiliários admitidos. Implementaram-se segurança e revisão interna, sem fabricar um tipo de título ou regra de propriedade.
|
||||
- Conferência visual desktop/móvel das fases aceites, ainda não concluída. O conector registou `The browser access host is unavailable`; não foram alterados navegadores ou controlos de segurança.
|
||||
- GD não está instalado neste PHP QA. Uploads, validação estrutural, armazenamento e autorização foram provados com PNGs válidos sem GD. O processador nativo mantém a geração de derivadas bloqueada quando GD/WebP não está disponível; não se declarou processamento de galerias públicas demonstrado.
|
||||
- A formatação global mantém os 61 ficheiros preexistentes fora do delta. Email real F2 e a abertura efectiva de publicação continuam com o seu estado anterior.
|
||||
|
||||
Próxima tarefa: apreciação desta entrega QA e disponibilização do exemplar aprovado do NDA, mantendo a publicação fechada. Qualquer instalação ou avanço para a Fase 5 exige autorização própria.
|
||||
@@ -0,0 +1,110 @@
|
||||
# Ofertas — Fase 5: fecho operacional em QA e aceitação das cinco fases
|
||||
|
||||
14/11/2026. Autorização expressa da Tempo para desenvolvimento exclusivo em QA. Base aceite com ressalvas: `e431de8b` (Fase 4). Mantêm-se `7691cab3`, `91c18573`, `1f2990e4` e o plano `6e302db6`. Referência funcional: RF-MN-003 da matriz existente.
|
||||
|
||||
**Fecho operacional implementado em QA. Publicação efectiva continua bloqueada. Nenhuma instalação em homologação/produção, alteração de dados operacionais, migração ou modificação em `mne`.** A aceitação desta entrega não é inferida da autorização para a desenvolver.
|
||||
|
||||
## Disponibilidade, retirada e reactivação
|
||||
|
||||
O dono registado da Oferta, com atribuição e representação ainda efectivas, pode retirar imediatamente a disponibilidade. A operação protectora não exige decisão editorial, MFA adicional do promotor ou uma versão de formulário actual: bloqueia o registo, reconfirma o dono e retira a versão corrente. Uma repetição não duplica a retirada. Não altera o conteúdo, apaga decisões ou converte o cadastro noutro produto.
|
||||
|
||||
Disponibilidade e estado editorial são distintos. Uma aprovação privada pode continuar registada, mas a retirada exclui a Oferta das consultas, detalhe, filtros, mapa e CSV e fecha a leitura empresarial protegida pelo NDA. Uma consulta anterior já entregue ao utilizador não pode ser recolhida retroactivamente.
|
||||
|
||||
Para voltar a disponibilizar, o proprietário pede nova validação no mesmo identificador. Abre-se um ciclo operacional que integra o hash do snapshot editorial (`private-offer-editorial-v4`); os contratos v2/v3 anteriores mantêm-se quando não há esse ciclo. O motor editorial existente conserva submissões, responsáveis, tarefas e decisões, reabre a preparação e exige submissão/revisão/aprovação independente. Só depois o proprietário pode repor a disponibilidade privada. Uma aprovação anterior ou um pedido de reactivação não bastam.
|
||||
|
||||
Uma retirada durante revisão também pode abrir esse novo ciclo, fechando as tarefas anteriores pelo mecanismo existente. Um primeiro rascunho retirado continua a exigir a sua primeira validação. Preparações F1 antigas sem snapshot seguem primeiro a vinculação editorial já existente, sem fabricar um histórico de referências anterior.
|
||||
|
||||
Reactivar disponibilidade **não publica**. O consumidor não reapresenta o snapshot público antigo: para um ciclo novo, exige a correspondência de `offer_cycle` e `editorial_hash` com o ciclo e a aprovação activados. Esses campos são o contrato de leitura futura, não um publicador. Nos testes, apenas projecções sintéticas são disponibilizadas por injecção interna na base isolada.
|
||||
|
||||
## Validade independente do NDA
|
||||
|
||||
`config/private_offers.php` acrescenta `validity_days` e `validity_policy_reference`, ambos inicialmente nulos. Não foi escolhido um prazo definitivo. Quando a Tempo definir um prazo, a configuração exige inteiro positivo e referência identificada; o limite técnico de 36.500 dias protege a aritmética e não é uma política comercial/jurídica.
|
||||
|
||||
A política é congelada por hash na aprovação, e a contagem começa no instante UTC da publicação dessa versão. A aprovação privada não inicia a contagem. Alterar a configuração ou aprovar uma edição privada não prolonga a validade do snapshot público anterior; o exemplar legado sem `editorial_hash` conserva a primeira política congelada. Uma política pendente não é substituída por zero ou por um prazo presumido.
|
||||
|
||||
No instante `agora >= publicação + duração`, a Oferta deixa de aparecer nas leituras. Essa exclusão não depende de cron, execução de um job, aceitação ou duração do NDA. A API acrescenta `expires_at` apenas quando a data é conhecida, e o cartão público mostra-a. O formato anterior do CSV foi preservado; os seus registos usam a mesma exclusão por disponibilidade/expiração. As respostas públicas de Ofertas usam `no-store` para não conservar uma versão retirada em caches HTTP.
|
||||
|
||||
O comando `investmoz:offers-expire` confere por defeito **sem escrita**. Com `--confirm`, materializa uma expiração já efectiva e regista o evento uma vez, com actor de sistema nulo, sem criar utilizadores. O agendamento é optativo por `expiry_scheduler_enabled`, que permanece **false**. Não foi activado no ambiente operacional. Os ensaios do comando ocorreram exclusivamente na base de testes guardada.
|
||||
|
||||
## Denúncias e Dashboard Colaborativo
|
||||
|
||||
A Oferta dispõe de formulário de denúncia fundamentada. O utilizador deve estar autenticado, com email verificado, e poder consultar a versão pública ou já ter acesso privado legítimo ao registo. Um estranho não usa este percurso para descobrir um rascunho. A fundamentação é obrigatória; limites técnicos de texto e o throttle existente limitam os pedidos.
|
||||
|
||||
Cada envio guarda identificador idempotente, autor, instante, versão do registo, hashes de referência e cópia cifrada do conteúdo referido. O fundamento também fica cifrado. Uma retoma com o mesmo actor/chave/conteúdo devolve o recibo sem criar outra tarefa, mesmo que entretanto a Oferta tenha sido retirada. Reutilizar a chave para outro conteúdo é conflito; outro actor não recebe o recibo.
|
||||
|
||||
O mesmo `OpportunityWorkflowTasks` cria uma tarefa CRM restrita, sem inventar oportunidade CRM, responsável ou transporte. A entrada **Ofertas — acompanhamento**, dentro do Dashboard Colaborativo existente, abre a listagem privada existente em modo operacional, com filtro de denúncias pendentes. Não há outro dashboard nem alteração do menu principal.
|
||||
|
||||
O validador interno com âmbito, MFA recente e sem conflito inicia a análise; fica associado à tarefa e à versão da denúncia. Conclui com fundamento, sem retirada ou com retirada da disponibilidade. Etag, versão da denúncia, estado e responsável são reconferidos no servidor sob bloqueio. O dono, auditor, outro âmbito ou outra conta não substituem essa autorização. A conclusão preserva os factos originais e o histórico; não é apresentada como decisão judicial ou certificação de fraude.
|
||||
|
||||
O proprietário não recebe o fundamento original ou os dados do denunciante pela API de acompanhamento. A leitura desses dados exige o papel interno referido. A origem cifrada permite conferir a versão denunciada, mesmo que a preparação corrente seja alterada. Notificações, tarefas, pesquisa pública, mapa e CSV não recebem o texto da denúncia.
|
||||
|
||||
## Preparação administrativa de destaques
|
||||
|
||||
O Publicador existente, com âmbito, MFA, autorização efectiva e sem conflito, pode guardar uma preparação administrativa com designação, datas previstas, fundamento e estado **rascunho/arquivado**. O plano tem versão, etag, histórico e auditoria próprios no contexto da mesma Oferta.
|
||||
|
||||
Não há estado comercial activo, alteração de `is_featured`, preço de destaque, cobrança, pagamento ou integração financeira. O proprietário e o público não recebem esse plano. A actualização de disponibilidade/denúncias preserva o plano local em edição; carregar o plano guardado exige uma acção explícita que identifica a substituição dos campos locais.
|
||||
|
||||
## Segurança, confidencialidade e compatibilidade
|
||||
|
||||
- Operações privadas reutilizam autenticação, perfis, representação, MFA, CSRF, limites de pedidos e respostas `no-store` existentes. As escritas usam transacções/bloqueio do registo; versões e etags impedem decisões sobre uma revisão desconhecida. A única dispensa de etag é a retirada protectora pelo dono, para não manter uma exposição por causa de um formulário antigo.
|
||||
- A retirada/expiração fecha imediatamente a leitura protegida da empresa. Uma nova validação tem hash/ciclo novo; não reaproveita silenciosamente a aceitação/autorização NDA anterior. O interessado conserva a possibilidade de retirar uma aceitação antiga durante revisão. Não se apagaram os seus registos históricos.
|
||||
- A Fase 4 mantém comprovativos apenas para validadores internos autorizados, originais cifrados, limite de seis fotografias, quarentena e análise de segurança. Não se acrescentou partilha externa de originais ou coordenadas exactas.
|
||||
- A projecção pública continua separada da edição privada; a barreira normal continua fechada. APIs nativas dos outros produtos não passam a devolver Ofertas. Filtros, detalhe, mapas e CSV de Ofertas usam a mesma condição de disponibilidade e expiração. A propriedade opcional `expires_at` não altera as chaves dos exemplares antigos sem validade definida.
|
||||
- Auditoria conserva actor, instante, versão, ciclo e identificadores de decisão. Fundamentos e origem das denúncias ficam cifrados; não há valores financeiros, contactos ou documentos nos títulos das tarefas ou nos eventos públicos.
|
||||
|
||||
## Verificação
|
||||
|
||||
Base: `offers_phase1_20261114_test`, loopback, porta **55441**, guardas existentes e dados exclusivamente sintéticos. Não foram utilizados processos reais, credenciais de negócio, publicação real ou entrega externa de email. O scanner sintético da Fase 4 mantém a identificação de teste e não foi registado no produto.
|
||||
|
||||
Os resultados e hashes finais encontram-se em `outputs/offers-private-phase5-20261114/qa-result.json`. A passagem inicial focada teve **12 testes/151 asserções aprovados**. A regressão integrada terminou com **59 testes/780 asserções aprovados**, F1–F5, com os assets estabilizados. Os grupos sobrepostos não devem ser somados.
|
||||
|
||||
| Grupo da passagem integrada | Testes | Asserções | Resultado |
|
||||
| --- | ---: | ---: | --- |
|
||||
| F1 — preparação privada | 14 | 146 | Aprovado |
|
||||
| F2 — circuito editorial | 11 | 161 | Aprovado |
|
||||
| F3 — consulta e filtros | 10 | 133 | Aprovado |
|
||||
| F4 — particularidades/confidencialidade | 11 | 160 | Aprovado |
|
||||
| F5 — fecho operacional | 13 | 180 | Aprovado |
|
||||
| **Total desta passagem, sem somar a inicial** | **59** | **780** | **Aprovado** |
|
||||
|
||||
Pint, PHPStan sem erros, TypeScript, lint/formatação dos oito ficheiros frontend e compilação QA passaram. A formatação global falhou nos mesmos 61 ficheiros preexistentes, fora do delta e com conteúdo Git inalterado. Não foram enfraquecidos testes, adicionadas excepções de análise ou instaladas dependências. A primeira passagem delimitada revelou uma recusa da antiga lista de acções à reabertura durante revisão: foi corrigida exclusivamente para o ciclo operacional pedido pelo dono, mantendo as regras dos restantes percursos.
|
||||
|
||||
A prova cobre retirada com formulário antigo, repetição, actor/âmbito incorrectos, novo ciclo e nova validação, ausência de reexposição do snapshot antigo, primeira preparação, retirada durante revisão, preservação do histórico, fronteira exacta da expiração, política nula/configuração congelada, NDA anterior fechado, idempotência da denúncia, fonte cifrada, tarefa/filas nativas, tratamento e conflitos de versão, MFA expirado, auditor, destaque administrativo sem efeitos comerciais e HTML compilado das páginas afectadas.
|
||||
|
||||
## Matriz de aceitação das cinco fases
|
||||
|
||||
| Fase | Resultado demonstrado em QA | Referência aceite/prova | Instalação/publicação | Ressalvas e pendências |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 1 — Preparação privada | Criar, guardar, recuperar, editar e submeter; autorização/isolamento; cadastro de Oportunidades reutilizado | `1f2990e4`; `PrivateOfferPreparationTest` | Não instalada | Visual pendente |
|
||||
| 2 — Circuito editorial | Dashboard existente, revisão, rejeição/correcção/reenvio, aprovação por versão, auditoria e notificações internas | `91c18573`, plano `6e302db6`; `PrivateOfferEditorialTest` | Não instalada; aprovação distinta de publicação | Visual e entrega real de email pendentes |
|
||||
| 3 — Consulta e filtros | Projecção aprovada/publicada sintética, categorias/localização/preços sem câmbio, mapa administrativo, API/CSV e preservação da versão anterior | `7691cab3`; `OfferPublicCatalogueTest` e compatibilidade F5 | Publicação efectiva bloqueada | Visual/móvel; abertura real não autorizada; limite de representação de 1.000 candidatos/30 territórios preservado |
|
||||
| 4 — Particularidades e confidencialidade | Campos confirmados, seis fotografias, comprovativos internos, duas condições NDA/vendedor, revogação/histórico e acessos por versão | `e431de8b`; `PrivateOfferConfidentialityTest` | Não instalada; NDA real inactivo | Exemplar/versão/aprovação do NDA; tipos jurídicos de comprovativo; GD/WebP; visual |
|
||||
| 5 — Fecho operacional | Retirada imediata, revalidação/reativação, validade configurável/expiração, denúncias/tarefas/Dashboard, destaques administrativos e compatibilidade | Esta entrega; `PrivateOfferOperationsTest` e recibo QA | Sem instalação ou activação comercial/pública | Apreciação desta entrega; prazo/política final por definir; pendências das fases anteriores preservadas |
|
||||
|
||||
Esta matriz integra o relatório solicitado e actualiza RF-MN-003 e o manual existentes; não é um novo inventário geral. O conjunto de Ofertas fica preparado em QA, mas não é declarado operacional em público, instalado, juridicamente fechado ou visualmente aceite. Procuras e o restante portal não são encerrados por esta entrega.
|
||||
|
||||
## Ficheiros alterados
|
||||
|
||||
| Grupo | Finalidade |
|
||||
| --- | --- |
|
||||
| `app/Services/OfferAvailability.php`, `PrivateOfferOperations.php` | Disponibilidade/validade, operações do dono, denúncias, tarefas, auditoria e planos administrativos |
|
||||
| `OpportunityProfileWorkflow.php`, `PrivateOfferPreparation.php`, `PrivateOfferConfidentiality.php` | Ciclo operacional no snapshot, reabertura/revalidação existentes e exclusão da leitura NDA |
|
||||
| `OfferPublicCatalogue.php`, `OfferPublicCatalogueController.php` | Exclusão consistente, contrato anterior preservado, validade opcional e cache HTTP |
|
||||
| `PrivateOfferOperationsController.php`, `PrivateOfferPreparationController.php`, `PortalEditorialWorkflowController.php`, `routes/web.php` | Endpoints privados, filas no âmbito e entrada no Dashboard existente |
|
||||
| `app/Console/Commands/ExpirePrivateOffers.php`, `routes/console.php`, `config/private_offers.php` | Conferência/materialização da expiração; prazo e agendamento sem activação por defeito |
|
||||
| `resources/js/components/offer-operations.tsx`, `public-offer-card.tsx`, `lib/public-offer.ts` | Acompanhamento privado e data pública opcional |
|
||||
| Páginas `offers/report`, `offers/preparation`, `offers/preparation-index`, `offers/public-detail`, `management/editorial-workflow` | Denúncia, retirada/reativação, tratamento e preparação administrativa integrados |
|
||||
| `tests/Feature/PrivateOfferOperationsTest.php` | Testes novos de segurança, percursos e compatibilidade |
|
||||
| Matriz RF-MN-003, manual, este relatório e recibo QA | Saldo e evidência das cinco fases |
|
||||
|
||||
## Limitações e decisões pendentes
|
||||
|
||||
1. **NDA:** a Tempo confirmou que ainda falta o exemplar/versão e a referência da aprovação. `private_offers.nda` mantém-se nulo. Não foi inventado texto jurídico ou uma assinatura com validade presumida.
|
||||
2. **Imóveis:** os tipos jurídicos de comprovativo admissíveis continuam por identificar. Upload/scan/revisão interna não certificam titularidade, licenciamento ou disponibilidade do bem.
|
||||
3. **Validade e divulgação:** a Tempo ainda não definiu prazo definitivo. A configuração permanece nula, sem presumir uma duração. Avisos jurídicos e eventuais condições comerciais de uma publicação futura não foram fabricados.
|
||||
4. **Visual:** a conferência desktop/móvel das fases anteriores permanece pendente. A última tentativa registada do conector devolveu `The browser access host is unavailable`. Não foram repetidos reinícios, desactivados controlos ou apresentados HTML/compilação como aceitação visual.
|
||||
5. **Imagens:** GD/WebP não está disponível neste PHP QA. Os originais privados e as validações da F4 foram demonstrados; geração/inspecção de derivadas e galeria pública não são declaradas concluídas. Não foram instaladas extensões.
|
||||
6. **Email:** a entrega real da F2 continua pendente. Não foram activados novos transportes, SMTP, SMS ou destinatários operacionais.
|
||||
7. **Qualidade preexistente:** a formatação global mantém os mesmos 61 ficheiros fora do delta. Lint focado, tipos e análise estática são registados separadamente dessa falha anterior.
|
||||
|
||||
Próximo passo: apreciação desta entrega QA e resolução das dependências documentais/visuais indicadas. **Qualquer instalação, configuração activa ou publicação aguarda autorização expressa da Tempo.**
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
# Ofertas — tentativa exacta de instalação privada e correcção QA
|
||||
|
||||
14/11/2026. Autorização: fast-forward de `main` de `96f51096` para **`3558647e8d82092cd0e31eefbe8b9e5e9eacf573`**, sem força, sem revisões posteriores e exclusivamente para homologação privada.
|
||||
|
||||
**Resultado: instalação interrompida antes da promoção. A homologação mantém `96f510965f70581a4b41dd94463f598b77dd7cf5`.** O ramo remoto recebeu exactamente `3558647e`; o executor verificou e construiu essa candidata, mas recusou-a na verificação TypeScript. Nenhuma revisão posterior foi enviada ou instalada.
|
||||
|
||||
## Execução e salvaguarda
|
||||
|
||||
1. Ancestralidade confirmada e `main` inicialmente em `96f51096`; fast-forward possível, árvore local sem alterações inesperadas.
|
||||
2. Janela reservada: path automático temporariamente pausado, serviço original inactivo e zero pedidos concorrentes. O lock existente `delivery.lock` foi adquirido sob o utilizador de implantação existente. O executor original não foi modificado: uma chamada única a `Delivery.deliver(3558647e)` reconfirmava o `main` real e lançava erro se encontrasse outro SHA, sem utilizar o ciclo que escolhe automaticamente revisões posteriores.
|
||||
3. Backup **novo**, de 14/11/2026 às **19:51:40 Africa/Johannesburg (17:51:40 UTC)**, em `/var/backups/investmoz-offers-exact-install-3558647e-20261114/backup`. Restauro isolado aprovado para 219 tabelas, 417 ficheiros e dois conjuntos de MFA; nenhuma base operacional substituída. Cópia da release anterior preservada e integridade reconfirmada.
|
||||
4. Push efectuado com referência exacta, sem força: `git push --porcelain origin 3558647e8d82092cd0e31eefbe8b9e5e9eacf573:refs/heads/main`. O remoto confirmou `96f51096..3558647e`; `ls-remote` reconfirmou o SHA integral. Os commits documentais posteriores não foram incluídos.
|
||||
5. O pedido do webhook foi reservado fora da fila automática durante a janela. O build decorreu numa release separada e numa base CI isolada; a ligação `current` nunca mudou. A reserva temporária foi relançada antes do build para utilizar o mesmo UMask `0027` do serviço existente; não se modificaram as permissões/configuração do serviço original.
|
||||
|
||||
| Salvaguarda nova | Resultado |
|
||||
| --- | --- |
|
||||
| Dump de 219 tabelas | SHA256 `88152dc39367f5e59eb072f63b44d3a949e93d73ae855eccb33c3c0b9e37bf6c` |
|
||||
| Arquivo de 417 ficheiros/configuração | SHA256 `aa6248e0eec3d27d58a4bbb3c16848a70dcb3524e6cafc6340b682984a8bbdb3` |
|
||||
| Restauro | Rede `none`, base nova `offers_install_restore_20261114_test`, porta interna 55441; contagens/checksums e ficheiros iguais; container de ensaio removido |
|
||||
| Integridade após a falha | 219/219 tabelas e 417/417 ficheiros sem diferenças face à cópia nova; sem alteração do código servido |
|
||||
|
||||
## Incidente concreto
|
||||
|
||||
O executor passou a preparação da candidata, Composer, guarda/schema isolado **1 teste / 3.106 asserções**, `npm ci` e build frontend. Parou em `npm run types:check`, antes dos unitários/frontend seguintes e antes de qualquer promoção ou `queue:restart` operacional.
|
||||
|
||||
O Wayfinder, com variantes de formulário, gerou em `resources/js/actions/App/Http/Controllers/PrivateOfferOperationsController.ts`:
|
||||
|
||||
- Linha 180: `export const reportForm` para a acção GET do formulário de denúncia;
|
||||
- Linha 331: `const reportForm` como auxiliar do formulário da acção POST `report`.
|
||||
|
||||
O TypeScript recusou as duas declarações com **TS2395**. Não é falha de persistência, autorização, GD, estrutura ou dados. A verificação QA simplificada anterior reutilizava rotas geradas e não exercitou esta geração completa; por isso a colisão não tinha sido detectada antes da entrega. A verificação completa do executor funcionou e conservou a aplicação anterior.
|
||||
|
||||
Release falhada preservada para diagnóstico: `/var/www/investmoz-homologacao/releases/3558647e8d82092cd0e31eefbe8b9e5e9eacf573-a4916dfe`. Logs protegidos e recibos da chamada exacta em `/var/lib/investmoz-deploy/offers-exact-3558647e-20261114`. Não se editou essa release para contornar os tipos ou apresentar como instalado um código diferente de `3558647e`.
|
||||
|
||||
## Recuperação e estado real
|
||||
|
||||
- O manifesto servido e o recibo de instalação continuam em **`96f51096`**.
|
||||
- Não houve promoção; **não foi necessário reverter código ou restaurar a base**. A release anterior permaneceu servida durante toda a tentativa.
|
||||
- O lock foi libertado, o path automático voltou ao estado activo original e a fila ficou sem pedidos pendentes. O pedido reservado foi preservado no registo privado da operação, sem reexecutar automaticamente a revisão falhada.
|
||||
- `main` permanece em **`3558647e`**, conforme o avanço autorizado. Não houve force push ou reversão implícita do ramo.
|
||||
- Zero migrações operacionais, importações, alterações de permissões, alterações de `mne`, activações de pagamentos, emails ou publicação de Ofertas. As configurações protegidas e os dados operacionais foram reconferidos sem diferenças.
|
||||
- As verificações pós-instalação do novo módulo em homologação não foram executadas: o módulo não chegou a ser instalado. Não se apresentam os testes QA como acesso autenticado real à nova versão no destino.
|
||||
|
||||
## Correcção mínima preparada apenas em QA
|
||||
|
||||
Dois pontos funcionais alterados:
|
||||
|
||||
| Ficheiro | Correcção |
|
||||
| --- | --- |
|
||||
| `app/Http/Controllers/PrivateOfferOperationsController.php` | Renomear a acção GET `reportForm` para `reportPage`; corpo e verificações intactos |
|
||||
| `routes/web.php` | Apontar o mesmo GET `/{id}/denunciar` à acção `reportPage` |
|
||||
|
||||
Não se alteraram os URLs, verbos HTTP, acção POST `report`, permissões, serviços, dados, histórico, menu ou as cinco fases. O Wayfinder gera agora `reportPage`/`reportPageForm` para GET e `report`/`reportForm` para POST, sem colisão. Nenhuma nova funcionalidade foi iniciada.
|
||||
|
||||
| Verificação da correcção | Resultado |
|
||||
| --- | --- |
|
||||
| `php artisan wayfinder:generate --with-form` | Aprovado, geração real de acções/rotas/formulários |
|
||||
| Build normal com `INVESTMOZ_QA_BUILD=0` | Aprovado; plugin Wayfinder activo, sem reutilização simplificada |
|
||||
| `npm.cmd run types:check` depois desse build | Aprovado |
|
||||
| PHPStan completo e Pint focado | Aprovados; zero erros PHPStan |
|
||||
| Operações, denúncias, confidencialidade/NDA e continuidade | **28 testes / 403 asserções aprovados**, incluindo GET do formulário e POST das denúncias |
|
||||
| Base do ensaio | `offers_wayfinder_fix_20261114_test`, loopback:55441, separada da prévia do utilizador |
|
||||
| Preservação | Base da prévia não foi limpa; prova aceite de continuidade guardada/restaurada byte a byte, nova fixture retida separadamente |
|
||||
|
||||
Os comandos, resultados e hashes constam de `outputs/offers-exact-install-20261114/qa-result.json`. A correcção não foi enviada para `main` nem instalada: a autorização recebida limitava a instalação a `3558647e` e proibia revisões posteriores.
|
||||
|
||||
## Saldo e decisão necessária
|
||||
|
||||
**Instalação de `3558647e`: BLOQUEADA pelo build completo.** Integração técnica e dados das cinco fases preservados; correcção de compatibilidade do gerador pronta em QA nesta entrega. Nova autorização deverá identificar a revisão correctiva exacta e o fast-forward a partir do `main` actual `3558647e`, conservando instalação exclusivamente privada e as guardas de revisão/janela/salvaguarda.
|
||||
|
||||
Continuam pendentes, separadamente, a conferência visual/móvel autenticada no destino, processamento completo/galeria, email real restrito a caixa autorizada e as decisões jurídicas sobre NDA, comprovativos, avisos e validade. A geração corrigida não encerra essas pendências nem autoriza publicação pública.
|
||||
@@ -0,0 +1,107 @@
|
||||
# Ofertas — parecer de prontidão para homologação privada
|
||||
|
||||
14/11/2026. Candidata técnica aceite: **`9a01426864a47dacd97db2b4da363aba86a94df6`**. Esta passagem autoriza exclusivamente preparação; não houve instalação, alteração operacional, migração, activação pública ou envio de email.
|
||||
|
||||
**Actualização da verificação autorizada posterior:** [parecer real pré-instalação](ofertas-verificacao-preinstalacao-20261114.md), **APTO COM CONDIÇÕES**. Destino `96f51096` conferido; backup/restauro de 219 tabelas e 417 ficheiros aprovado; GD/WebP disponível. As referências abaixo a destino/backup ainda por conferir descrevem a preparação original. Mantêm-se nova autorização de instalação e condição de revisão exclusiva em `main`; nenhuma instalação executada.
|
||||
|
||||
## Parecer
|
||||
|
||||
**Existe validação integrada em QA suficiente para recomendar uma instalação controlada de avaliação privada, com condições de entrada. Não existe aceitação integral para operação/publicação pública.** A autorização para instalar continua separada e ainda não foi recebida nesta passagem.
|
||||
|
||||
O percurso demonstrado inclui criação/recuperação da Oferta, submissão, revisão/aprovação independente, projecção pública exclusivamente sintética, contexto privado autorizado no mesmo dossier, localização, hipóteses económicas declaradas, cálculo, relatório HTML/Word e recuperação. NDA e autorização do vendedor foram testados com documentos sintéticos; não conferem autorização para copiar a informação empresarial confidencial para o contexto. A prova não demonstra um investidor público a converter uma Oferta operacionalmente publicada num dossier: esse publicador não está disponível.
|
||||
|
||||
Condições de entrada para a futura instalação: confirmar revisão/schema/configuração/processos reais do destino; concluir backup actual e ensaio de restauro; rever o delta exacto e a reversão compatível; receber autorização expressa para a candidata e o destino. Se uma destas condições falhar, não se instala. Visual, correio real e derivadas de imagens ficam como verificações de homologação explicitamente pendentes, sem declarar o módulo concluído.
|
||||
|
||||
## Integridade e provas reutilizadas
|
||||
|
||||
Confirmada a ancestralidade F1 `1f2990e4`, plano `6e302db6`, F2 `91c18573`, F3 `7691cab3`, F4 `e431de8b`, F5 `528a2597`, auditoria `ed2fd86f` e correcção `9a014268`. **48 hashes** dos dois recibos de código/provas conferidos contra os respectivos blobs Git, todos correspondentes. Conferir na versão de cada recibo evita interpretar as actualizações documentais posteriores como corrupção das provas anteriores.
|
||||
|
||||
| Evidência aceite | Resultado / limite |
|
||||
| --- | --- |
|
||||
| F1–F5, passagem integrada original | 59 testes / 780 asserções, aprovados; não repetidos nesta preparação |
|
||||
| Correcção e compatibilidade afectada | 22 / 461 aprovados; inclui 4/62 de continuidade, 11/161 de confidencialidade, 4/192 de dossiers e 3/46 de gestão nativa |
|
||||
| Plano financeiro e relatórios frontend | 41 testes aprovados |
|
||||
| Prova ligada ao JSON da execução PHP | 1 teste aprovado; cálculo por custos declarados e HTML/Word gerados/reabertos |
|
||||
| PHPStan, tipos, lint/formatação focados | Aprovados no recibo aceite; não houve alteração funcional nesta preparação |
|
||||
| Imagens sem GD | Provas aceites de upload, limite, quarentena e recusa 423 de processamento; não equivalem a validação de derivadas |
|
||||
|
||||
Recibo desta preparação: `outputs/offers-homologation-preparation-20261114/readiness-verification.json`. As provas anteriores mantêm-se nas pastas referidas pela matriz e pelo [recibo da correcção](ofertas-correcao-contexto-qa-20261114.md). Não somar passagens sobrepostas nem usar quantidade de testes como conclusão do portal inteiro.
|
||||
|
||||
## Estrutura, dependências e configuração
|
||||
|
||||
Foi comparada a candidata com a revisão **histórica** do recibo de homologação `96f51096`: **zero diferenças em `database/migrations`, `composer.lock` e `package-lock.json`**. Também não há migrações novas no delta das cinco fases/correcção. Não é necessário criar uma migração de Ofertas. Esta comparação de ficheiros não prova a aplicação das estruturas no servidor actual.
|
||||
|
||||
Pré-conferência em leitura no destino: registo de migrações e estrutura efectiva de `opportunities` (gestão, dono, organização, JSON editorial), perfis/permissões e revisão de acesso, recursos `opportunity_images/documents/locations`, `documents`, auditoria, tarefas, notificações editoriais com `opportunity_id/opportunity_version/content_hash`, dossiers/contexto e rascunhos de simulação. Conferir nome/schema das relações pela versão instalada, constraints, cifra e acesso ao armazenamento. As dependências de recursos e contexto identificam, entre outras, `2026_10_09_030000_extend_opportunity_resources` e `2026_10_16_150000_create_investor_navigation_preferences`; notificações dependem de `2026_11_13_223000_extend_editorial_notifications_to_opportunities`.
|
||||
|
||||
**Não executar indiscriminadamente `artisan migrate`:** existem migrações anteriores guardadas para QA isolado, incluindo a das notificações. Se faltar estrutura no destino, parar e apresentar o delta aditivo e autorização concreta; não remover guardas para aplicar um ficheiro. Não usar `migrate:fresh`, seeders, rollback estrutural destrutivo ou alterações a `mne`.
|
||||
|
||||
Manter versões e locks existentes. O PHP CLI QA é 8.5.11; a compatibilidade real deve ser conferida com as dependências bloqueadas, PHP-FPM/web e worker, PDO PostgreSQL/PostGIS, memória, discos privados, cifra/APP_KEY existente, scanner, fila e scheduler. Não instalar ferramentas/extensões nem actualizar versões nesta preparação. Confirmar manifesto/assets e ausência de `public/hot` no artefacto a instalar; não publicar `.env`, ficheiros privados, dependências locais ou fixtures.
|
||||
|
||||
Para avaliar o circuito privado, confirmar os mecanismos existentes `portal.profile_access_enabled` e `editorial_workflow.enabled` e respectivos papéis efectivos. Não os activar globalmente ou conceder permissões por inferência; diferenças para o destino têm de constar da autorização de instalação. Preservar autenticação, MFA, representação, CSRF, throttling, auditoria e transportes dos outros módulos.
|
||||
|
||||
## Publicação integralmente fechada
|
||||
|
||||
- `Opportunity` exige rascunho e `published_at=null` para Ofertas e impede remover o marcador privado; o scope público nativo exclui-as.
|
||||
- Aprovar não publica; o workflow privado e percursos alternativos mantêm a recusa.
|
||||
- `OfferPublicCatalogue` normal não devolve Ofertas. A injecção sintética exige ambiente de testes e identidade efectiva da base isolada; não há variável HTTP/ambiente de activação equivalente nem publicador operacional para ligar.
|
||||
- Manter `private_offers.nda=null`, validade/política nulas e `expiry_scheduler_enabled=false`; não copiar documentos/snapshots sintéticos para o destino. O agendamento de expiração não é necessário para instalar em avaliação privada.
|
||||
- Pós-instalação deve comprovar ausência em lista, detalhe directo, APIs, mapa, pesquisa nativa e CSV, incluindo após aprovação privada. Não basta ocultar um menu.
|
||||
|
||||
## GD e preservação de uploads
|
||||
|
||||
A conferência local nesta passagem confirmou PHP 8.5.11 **sem GD, `imagecreatetruecolor` ou `imagewebp`**. O destino actual não foi consultado: não se presume que possua a mesma configuração. `OpportunityImageProcessor::prepare` verifica a disponibilidade e devolve 423 **antes** da transacção/escrita; originais e metadados permanecem. Upload, cifra, quarentena e antimalware são percursos separados; não se deve substituir ficheiros existentes ou desactivar scan para gerar uma galeria.
|
||||
|
||||
Sem GD/WebP, é possível instalar para avaliação privada com esta limitação expressa, mas não aceitar processamento de derivadas/galeria. Depois de autorização adequada e num processo compatível, testar JPEG/PNG/WebP sintéticos, dimensões e memória, derivadas sem metadados sensíveis, scans individuais, hash, cifra, limite de seis e restauro lógico. Conferir os três runtimes (web/CLI/worker). Não reprocessar em lote os uploads existentes. Alteração de extensão/runtime fica fora desta preparação e exige delta próprio se necessária.
|
||||
|
||||
## Conferência visual e móvel autenticada preparada
|
||||
|
||||
Usar apenas contas e registos sintéticos identificados na homologação; autenticação e MFA normais, sem códigos na conversa. Preparar dono promotor, revisor independente com âmbito e MFA e interessado sem acesso privado; não criar/grantar contas no activo nesta passagem.
|
||||
|
||||
| Sessão / janela | Conferência e critério |
|
||||
| --- | --- |
|
||||
| Dono, desktop 1366×768 | Criar/guardar/recuperar, endereço e Oferta correctos, erros junto dos campos, descrição sem repetição/perda; subir recursos sintéticos até ao limite; botão visível; voltar sem perder edições |
|
||||
| Revisor independente | Fila do Dashboard existente → Oferta exacta → decisão fundamentada por versão; conflitos e auto-aprovação recusados; nenhuma publicação |
|
||||
| Dono, dossier/simulador | Mesma referência/localização, prévia e confirmação, manter resumo deliberado; rascunho/resultados e relatório recuperáveis |
|
||||
| Janela 390×844 e 360×800 | Sem scroll horizontal impeditivo; menus, modais, campos, erros, anexos e acções acessíveis por toque/teclado; foco e retorno preservados |
|
||||
| Conta sem âmbito/interessado | Rotas directas, contexto, ficheiros e APIs recusados; nenhum texto confidencial ou comprovativo no HTML/relatório; catálogo público vazio |
|
||||
| Retirada/revisão | Estado visível e coerente, indisponibilidade imediata; reactivação exige nova validação; histórico preservado |
|
||||
|
||||
Registar verificador, instante, candidata servida, viewport, conta/papel sintético, rota e resultado, com capturas sem credenciais. Última evidência disponível do conector: `The browser access host is unavailable`; não foi feita nova tentativa nesta preparação. Quando o conector funcionar, identificar aba/URL antes de actuar; se continuar indisponível, usar conferência manual registada, sem ciclos de reinício ou alterações de segurança. Não converter HTML compilado em aceitação visual.
|
||||
|
||||
## Prova de email real preparada, não executada
|
||||
|
||||
O job existente `NotifyPrivateOfferDecision` requer notificações activas, destinatário na allowlist, Oferta válida e permissão efectiva reconferida antes do envio. O corpo contém referência e ligação protegida, sem descrição, contacto ou valores empresariais. Usa o transporte existente; não se acrescenta SMS ou outro motor.
|
||||
|
||||
Ensaiar preferencialmente numa cópia isolada com código/configuração SMTP equivalentes e dados exclusivamente sintéticos, saída de correio restrita a **uma caixa de teste indicada pela Tempo**. A caixa ainda não foi designada; não assumir um endereço operacional. Provisionar segredos pelo mecanismo existente, sem exportá-los no pacote. Não copiar utilizadores reais, consumir a fila partilhada nem mudar a allowlist/flag global da homologação afectando os restantes serviços.
|
||||
|
||||
Depois de autorização do ensaio, criar decisão sintética/notificação pelo serviço existente e processar apenas o job identificado. Conferir aceitação pelo transporte e recepção efectiva na caixa, referência/ligação correcta, remetente, ausência de dados reservados e comportamento após perda de permissão. Guardar ID técnico/horário e resultado, sem credenciais. A aceitação SMTP isolada não basta como entrega na caixa. Se houver reenvio por falha, registar tentativas; não prometer entrega exactamente uma vez, pois o job admite retries. Não activar notificações globais ou escoar filas acumuladas para efectuar esta prova. Nesta preparação não se enviou email.
|
||||
|
||||
## Decisões jurídicas ainda necessárias
|
||||
|
||||
1. **NDA:** exemplar e versão aprovados, referência da decisão e âmbito aplicável. O mecanismo técnico não inventa texto, assinatura válida ou concessão de transferência para relatórios. Mantém-se fechado enquanto ausente.
|
||||
2. **Imóveis:** tipos de comprovativo admissíveis e tratamento a dar à titularidade/representação declarada; acesso continua interno, não se certifica direito ou licença por upload/scan.
|
||||
3. **Avisos/validade:** texto aplicável à divulgação/interesse, responsabilidades e confidencialidade; prazo/política final de validade. Não escolher um prazo ou inventar exclusões jurídicas. São decisões para os comportamentos dependentes/publicação, não motivos para refazer as provas privadas aceites.
|
||||
|
||||
## Cópia, instalação futura e reversão
|
||||
|
||||
**Preparação material:** bundle Git local da candidata `9a014268`, com pré-requisito histórico `96f51096`, em `outputs/offers-homologation-preparation-20261114/offers-9a014268.bundle`; manifestos/hashes na mesma pasta. Não foi enviado ao servidor. Confirmar que o destino contém o pré-requisito e que o delta não sobrepõe alterações posteriores; não instalar por `reset --hard` nem substituir árvore com trabalho alheio.
|
||||
|
||||
1. **Antes de qualquer escrita:** após autorização concreta, identificar destino/revisão/schema/configuração e conferir delta completo. Qualquer diferença de estrutura, configuração partilhada, revisão ou permissões não abrangida interrompe a instalação para decisão específica.
|
||||
2. **Backup actual:** cópia consistente da base, ficheiros privados e manifestos, configuração e chaves protegidas, revisão anterior e artefactos. Não imprimir segredos. Conferir hash/identificadores/contagens e ensaiar restauro isolado com leitura de documentos cifrados e histórico. O backup antigo de `96f51096` é referência histórica e não serve como backup actual. **Nenhum novo backup ou restauro do destino foi efectuado nesta passagem.**
|
||||
3. **Executar pelo processo Git existente:** candidata e manifesto aprovados, dependências fixadas, assets próprios de produção e caches/workers conforme procedimento existente. Não executar migrações por rotina; não activar publicação, NDA, agendamento, destaques ou correio partilhado. Manter dados e integrações existentes.
|
||||
4. **Conferir após instalar:** SHA servido/assets, HTTP e logs, autenticação/MFA/âmbito, circuitos privados sintéticos, permanência dos bloqueios públicos, integridade dos uploads e compatibilidade com oportunidades/dossiers, instituições e demografia existentes. Não reexecutar importações ou publicação de conteúdo para provar integridade. Registar diferenças de runtime/cache/sessões separadamente de dados de negócio.
|
||||
5. **Reverter antes de uso:** se não existirem escritas novas de Ofertas e a revisão anterior for compatível, repor apenas código/assets/configuração alterados no âmbito aprovado e conferir novamente; conservar logs e cópias.
|
||||
6. **Reverter depois de uso:** não regressar cegamente à F4 ou revisão que ignore retirada/expiração, hashes v4 ou confidencialidade. A F4 não testa `OfferAvailability` no leitor protegido. Usar recuperação compatível que preserve as guardas novas e todo o JSON/histórico/tarefas/notificações. Se não existir revisão de recuperação comprovada, suspender o acesso afectado pelo mecanismo administrativo já autorizado, sem enfraquecer autenticação e sem inventar um interruptor; preparar correcção em frente. Não apagar estados para acomodar código antigo.
|
||||
7. **Restauro de dados:** não restaurar indiscriminadamente sobre actividade posterior. Delimitar o incidente, preservar as novas escritas e aprovar reconciliação/restauro próprio. Sem `down` destrutivo, perda de decisões ou alterações a `mne`.
|
||||
|
||||
## Matriz de decisão final
|
||||
|
||||
| Plano | Estado actual |
|
||||
| --- | --- |
|
||||
| Aprovação técnica F1–F5 e continuidade em QA | Aceite com as ressalvas documentadas; integridade reconfirmada |
|
||||
| Instalação privada de avaliação em homologação | Tecnicamente recomendável, condicionada ao preflight real, backup/restauro, delta/reversão e nova autorização expressa |
|
||||
| Instalação efectuada / ambiente actualmente comprovado | Não; revisão/schema/runtime actuais do destino não verificados nesta passagem |
|
||||
| GD/derivadas, visual/móvel e email real | Pendentes; não impedem por si só instalar código fechado para avaliação, mas impedem declarar essas funções aceites |
|
||||
| Publicação operacional / NDA real | Bloqueados; sem autorização e sem activação |
|
||||
|
||||
Próxima acção: decisão da Tempo sobre a instalação privada da candidata e condições acima. Esta preparação não executa essa decisão nem declara concluídas as verificações no destino.
|
||||
@@ -0,0 +1,105 @@
|
||||
# Ofertas — verificação real pré-instalação
|
||||
|
||||
14/11/2026. Autorização exclusiva de inspeção, cópia actual e restauro isolado; nenhuma autorização de instalação ou publicação. Referências: preparação `d954c83b`, integração tecnicamente aceite `9a014268`, menus QA `3558647e`.
|
||||
|
||||
## Parecer: APTO COM CONDIÇÕES
|
||||
|
||||
**Execução posterior autorizada e interrompida:** [incidente do build Wayfinder e correcção QA](ofertas-instalacao-privada-incidente-wayfinder-20261114.md). O fast-forward exacto para `3558647e` foi autorizado e efectuado; o executor recusou a candidata por TS2395 antes da promoção. Homologação preservada em `96f51096`, 219 tabelas/417 ficheiros intactos, sem migração ou reversão. Correcção mínima validada apenas em QA, dependente de nova autorização de revisão. Os estados de entrada abaixo descrevem verificações anteriores e não substituem este resultado de execução.
|
||||
|
||||
**Tentativa após autorização expressa de instalar `3558647e`: BLOQUEADA ANTES DE INICIAR.** A condição 4 da autorização determina interromper quando o executor usa o último `main` e este difere da candidata. `git ls-remote` confirmou `main=96f510965f70581a4b41dd94463f598b77dd7cf5`, enquanto a revisão autorizada é `3558647e8d82092cd0e31eefbe8b9e5e9eacf573`. A homologação continua em `96f51096`, executor inactivo, zero pedidos. Não se fez push, pedido, instalação, migração ou reversão. A autorização de instalação foi recebida; falta decidir a integração controlada exacta em `main` depois desta paragem. Existe fast-forward possível, sem força ou inclusão dos commits documentais posteriores, mas o push pode accionar o executor e não foi executado. Evidência: `outputs/offers-authorised-install-20261114/entry-blocked.json`. O parecer técnico e o restauro anteriormente comprovado mantêm-se; esta é uma precondição operacional concreta, não uma reprovação dos testes.
|
||||
|
||||
O destino é compatível com a **avaliação privada** das Ofertas. As estruturas verificadas estão presentes, não há migrações/dependências novas no delta, GD/WebP está disponível no destino e o backup/restauro actual passou. A conclusão não autoriza instalar nem declarar aceite a publicação operacional.
|
||||
|
||||
A instalação futura tem de identificar uma revisão exacta e ocorrer numa janela sem integrações concorrentes: o executor actual segue o último `main`, em vez de fixar o SHA indicado no pedido. Se essa condição não puder ser garantida, **não se cria o pedido de instalação nem se publica a candidata em `main`**. Nenhuma alteração ao executor ou às suas configurações foi feita.
|
||||
|
||||
## Destino e compatibilidade comprovados
|
||||
|
||||
| Item | Observado no destino real |
|
||||
| --- | --- |
|
||||
| Endereço / revisão instalada e servida | `https://teste-investidor.tempo.co.mz` — `96f510965f70581a4b41dd94463f598b77dd7cf5` |
|
||||
| Release resolvida | `/var/www/investmoz-homologacao/releases/96f510965f70581a4b41dd94463f598b77dd7cf5-8ee72062` |
|
||||
| Base real | `teste_investidor`, porta 5432; PostgreSQL 17.11; PostGIS 3.6.4; pgcrypto 1.3 |
|
||||
| Aplicação | PHP CLI 8.4.27; Nginx, PHP-FPM 8.4, PostgreSQL, fila existente e ClamAV activos |
|
||||
| Executor | `/var/www/deploy/investmoz-pipeline.py`, serviço inactivo, path activo, zero pedidos pendentes na observação |
|
||||
| Configuração | Perfis e circuito editorial activos; notificações desactivadas; zero destinatários autorizados; nenhuma configuração alterada |
|
||||
| Segurança observada | `APP_ENV=local`, debug falso; cookies Secure e HttpOnly, SameSite lax; URL HTTPS; sem overrides APP_ENV/APP_DEBUG encontrados no FPM/Nginx |
|
||||
| Reserva global do ambiente | `homologation_only=false`, modo configurado `perimeter`; não se presume reserva global activa a partir do nome do modo. As Ofertas privadas continuam a exigir autenticação/perfis/âmbito próprios. Não foi alterada esta configuração. |
|
||||
| Capacidade inicial | Cerca de 24,6 GB livres; condição de capacidade do executor a reconferir antes de instalar |
|
||||
|
||||
Foram verificadas 19 relações de suporte, sem ausências nas colunas requeridas: Oportunidades/gestão/JSON/dono/organização, utilizadores e revisões de acesso, perfis, pedidos/aprovações de segurança, recursos, documentos/cifra/scan, edições/notificações, auditoria, tarefas, dossiers/contexto e rascunhos. Confirmados a FK das notificações, `portal_editorial_notice_subject` e `portal_editorial_op_notice_uq`. O destino regista 112 migrações.
|
||||
|
||||
O delta `96f51096..3558647e` não contém diferenças em `database/migrations`, `composer.lock` ou `package-lock.json`. **Não se prevê executar qualquer migração operacional.** Não se aplicaram seeders, concessões de permissões, alterações a `mne` ou alterações de estrutura no destino.
|
||||
|
||||
Evidências sanitizadas em `outputs/offers-preinstall-verification-20261114/target-runtime-schema.json`, `constraints-automatics.json`, `security-runtime.json` e `executor-status.json`. A release não é um checkout Git: a revisão foi confirmada pelo recibo do executor e pelo manifesto servido, não por um `git status` vazio.
|
||||
|
||||
## Publicação e automatismos
|
||||
|
||||
O módulo ainda não está instalado: não existem classes de preparação/operações/catálogo de Ofertas no destino, não há rotas `ofertas/offers` registadas, preparações privadas, projecções públicas ou registos publicados de Ofertas. Não há jobs ou falhas de `NotifyPrivateOfferDecision` na fila observada.
|
||||
|
||||
Na candidata, mantêm-se as barreiras aceites em QA: o modelo conserva o marcador privado e exige `draft`/`published_at=null`; o scope nativo exclui Ofertas; aprovação não publica; os consumidores públicos normais permanecem fechados; listagem/detalhe/API/mapa/CSV só foram demonstrados com injecção sintética guardada pela identidade da base isolada. A base real 5432 não satisfaz essa guarda. Nenhuma rota ou variável de ambiente foi aberta para executar esta verificação.
|
||||
|
||||
NDA permanece sem exemplar configurado e o agendamento optativo de expiração está desactivado. A instalação privada deve preservar estes estados. As provas de bloqueio das cinco fases e a correcção integrada foram reutilizadas; não se criaram Ofertas ou ensaios de publicação no destino real para voltar a testar o que já foi aceite.
|
||||
|
||||
## Backup actual e restauro executados
|
||||
|
||||
Backup criado em **14/11/2026 às 19:34:37, Africa/Johannesburg (17:34:37 UTC)**, em `/var/backups/investmoz-offers-preinstall-20261114/backup`, directório privado com modo 0700. A fonte foi lida por transacções `REPEATABLE READ READ ONLY`, com snapshot exportado partilhado entre o dump e as contagens/checksums. O arquivo inclui todos os schemas de aplicação; não se limitou a `public`.
|
||||
|
||||
| Prova | Resultado |
|
||||
| --- | --- |
|
||||
| Dump consistente | 219 tabelas; SHA256 `662c5d9f1fe7e01a5ccf7529c8c8e8f27205907d60d2da75a580b7b6c37c252e` |
|
||||
| Ficheiros persistentes e configuração | 417 ficheiros verificados; arquivo SHA256 `f438f31f212e516442526b609bf5ac02cfd2f630a6ac2a2b6fc12f1f88717a42` |
|
||||
| Código, assets e dependências da release anterior | Arquivo de 9.873 ficheiros regulares, 25.862.122 bytes; SHA256 `914bc32627a51ec9001d07e5ac31d5717719a4a7f93e43dc26189865962735ca` |
|
||||
| Restauro da base | Nova instância `offers_preinstall_restore_20261114_test`, porta interna 55441, imagem PostGIS já existente e fixada por digest, rede `none`, sem porta publicada |
|
||||
| Comparação do restauro | 219/219 tabelas com contagens/checksums exactamente iguais ao snapshot |
|
||||
| Recuperação dos ficheiros | 417/417 ficheiros extraídos apenas para directório privado de ensaio e verificados por tamanho/hash; nenhuma extracção sobre a aplicação |
|
||||
| Chaves e envelopes | Dois conjuntos de MFA recuperados com as chaves preservadas; zero documentos activos com `storage_encryption=laravel` nesta fonte, pelo que não se inventa uma prova de decifragem de originais inexistentes |
|
||||
| Limpeza do ensaio | Container/volume criado para a prova removido após validação do seu identificador e isolamento; sem tocar no cluster real ou no QA local da prévia |
|
||||
|
||||
Configuração, segredos e dados do backup permaneceram no directório privado do servidor; não foram colocados no repositório ou nas respostas. Os recibos locais contêm apenas metadados e resultados sanitizados. O restauro usa `pg_restore` para reproduzir o snapshot na instância nova; não executa migrações do produto nem altera o schema `mne` operacional.
|
||||
|
||||
Na verificação posterior, os 9.873 ficheiros da release e os ficheiros persistentes/configuração mantêm-se iguais. Das 219 tabelas, 218 permanecem iguais ao backup; a diferença é uma sessão adicional. A reconciliação confirmou **51 sessões anteriores integralmente preservadas e sem alteração de campos, zero removidas, 52 actuais**. Não se sobrescreveu esta actividade concorrente para obter igualdade artificial. As operações desta verificação não executaram escritas de dados operacionais.
|
||||
|
||||
Recibos: `backup-summary.json`, `restore-summary.json`, `target-final-preservation.json`, `data-preservation.json`, `session-reconciliation.json`. Os procedimentos revistos de backup/restauro estão na mesma pasta de evidências. A cópia é válida para o instante registado; se a instalação ocorrer posteriormente, reconferir actividade e actualizar a salvaguarda quando necessário.
|
||||
|
||||
## Imagens e email
|
||||
|
||||
**GD/WebP:** presentes no CLI e no PHP-FPM do destino. O ensaio em memória de PNG 400×400 → descodificação → WebP → conferência de dimensões passou, sem ler/alterar uploads ou criar documentos operacionais. A ausência de GD continua a ser uma característica do PHP QA local, não um bloqueante de dependências deste destino. Isto não substitui a futura aceitação do processamento completo das Ofertas, quarentena/scans, derivadas, metadados e galeria com exemplos autorizados.
|
||||
|
||||
**Email:** SMTP configurado, mas `portal.notifications_enabled=false` e lista autorizada vazia. Nenhum email foi enviado. O job de Ofertas reconfirma destinatário na allowlist e acesso à versão. Falta designar uma caixa de teste e autorizar o ensaio/configuração correspondente. Preferir instância isolada com dados sintéticos, destinatário controlado e apenas o job identificado; não activar a flag partilhada nem escoar a fila acumulada para provar entrega. As actuais condições impedem o envio de Ofertas a utilizadores operacionais. A recepção real continua por verificar.
|
||||
|
||||
## Operações exactas previstas — não executadas
|
||||
|
||||
1. **Decisão da Tempo:** autorizar um SHA concreto. A integração aceite é `9a014268`; a prévia com menus é `3558647e8d82092cd0e31eefbe8b9e5e9eacf573`. O bundle congelado da preparação anterior contém apenas `9a014268`. Para instalar o que foi mostrado na prévia, a autorização terá de incluir expressamente `3558647e`; não substituir silenciosamente uma candidata pela outra.
|
||||
2. **Entrada controlada:** reconfirmar revisão instalada `96f51096`, ficheiros sem drift, estruturas/flags, capacidade, backup/restauro e fila vazia. Acordar janela sem integrações concorrentes em `main`. Se não houver garantia de revisão exclusiva, não iniciar o executor.
|
||||
3. **Integração Git:** só depois da autorização, fazer chegar a revisão exacta ao ramo `main` de `nala/Investidor-V2` pelo processo existente; não publicar simplesmente o HEAD local, não criar pedidos nesta verificação. A actualização pode desencadear o path activo, pelo que o push faz parte da instalação autorizada, não desta preparação.
|
||||
4. **Executor existente:** `fetch` do ramo, verificação de drift e capacidade; criar release nova sob `/var/www/investmoz-homologacao/releases`; usar base QA isolada e versões fixadas; executar Composer sem scripts, descoberta de pacotes, guarda da base, suite estrutural `phpunit.investmoz.xml`, `npm ci`, build, tipos, deployment guard, unitários e testes frontend. Depois instalar vendor de produção a partir do mesmo lock e conferir plataforma. Nenhum `artisan migrate` operacional está previsto.
|
||||
5. **Preparação da release:** validar ausência de `public/hot`; ligar os caminhos persistentes `.env`, `storage` e `public/storage` existentes; gerar o manifesto da candidata e aplicar apenas as permissões técnicas já previstas pelo executor aos directórios novos. Não alterar papéis/permissões de negócio nem configuração funcional.
|
||||
6. **Promoção:** reconfirmar SHA e ausência de drift, trocar `current` atomicamente e verificar manifesto/homepage; só depois reiniciar a fila pelo comando existente `queue:restart` e actualizar `previous.json`/`installed.json`. Estas escritas de runtime/cache devem constar do recibo, separadas dos dados de negócio. Não executar publicação, importações, agendamento ou ensaio de email.
|
||||
7. **Pós-verificação:** confirmar revisão/assets, serviços, logs, portas e preservação; conferir fecho público por todas as vias, acesso privado autorizado/recusado, continuidade no dossier e uploads com exemplares sintéticos autorizados; reconciliar cache/sessões e contagens/hash de dados protegidos. Registar quem fez a conferência desktop/móvel, revisão e resultado. Não concluir visual ou entrega de correio por inferência.
|
||||
|
||||
**Risco concreto do executor:** nas linhas 263–266, um avanço do ramo durante o build cria pedido para a revisão mais recente; nas linhas 290–298, os pedidos são processados seguindo `latest()`, não fixando o SHA nominal do ficheiro. Não se apresenta isto como uma recusa automática de revisões não autorizadas. É condição de utilização garantir a janela exclusiva e a coincidência do único `main` com a candidata autorizada; sem isso, a instalação fica bloqueada. A correcção do executor não foi iniciada nem autorizada nesta tarefa.
|
||||
|
||||
## Interrupção e reversão
|
||||
|
||||
Interromper antes de qualquer promoção se mudar a revisão/ramo, aparecer pedido concorrente ou drift, faltar espaço, divergir estrutura ou backup/restauro, falhar um teste/asset/guarda, houver proposta de migração/permissão/configuração não autorizada ou não for possível conservar publicação/notificações fechadas. Uma revisão nova de `main` invalida o âmbito concreto; não a aceitar como parte implícita da autorização.
|
||||
|
||||
O executor existente preserva a release servida durante o build e verifica drift antes da troca. Dispõe de reversão por `previous.json` e troca atómica, com health-check e reinício da fila; **nenhuma reversão foi executada no destino nesta tarefa**. A inspecção da sua implementação não equivale a ensaio de comutação na homologação.
|
||||
|
||||
- **Antes de qualquer uso das Ofertas:** o destino tem zero Ofertas; se isso se mantiver e não houver jobs/estados novos, uma reposição da release/configuração anterior compatível pode ser autorizada com os artefactos preservados, sem restaurar toda a base.
|
||||
- **Depois de utilização:** não regressar cegamente a `96f51096` ou F4: desconhecem parte dos estados/guardas novos. Uma retirada, expiração, denúncia, versão ou grant novo exige recuperação compatível que mantenha os bloqueios e históricos, preferindo correcção em frente. Não apagar dados ou jobs para acomodar código antigo.
|
||||
- **Incidente de dados:** preservar a actividade posterior e delimitar uma reconciliação/restauro próprio. Não substituir a base por este dump sem autorização adicional. Não usar migrações `down` nem rollback destrutivo de schema.
|
||||
|
||||
## Saldo e autorização necessária
|
||||
|
||||
| Dimensão | Estado |
|
||||
| --- | --- |
|
||||
| Integração técnica em QA e menus | Provas aceites preservadas; menus QA verificados; sem nova ronda geral |
|
||||
| Destino, estruturas, GD e capacidade | Conferidos, compatíveis para avaliação privada |
|
||||
| Backup actual e restauro isolado | Aprovados, com reconciliação da sessão nova e recuperação das chaves |
|
||||
| Instalação privada | **Não autorizada/não executada; APTO COM CONDIÇÕES** |
|
||||
| Condição operacional do Git | SHA autorizado e janela sem integrações concorrentes obrigatórios; caso contrário bloquear |
|
||||
| Publicação operacional | Bloqueada; não autorizada; publicador efectivo não implementado |
|
||||
| Visual/móvel, galeria e email real | Pendentes de conferência/ensaio específicos; sem utilizadores operacionais notificados |
|
||||
| NDA, comprovativos e avisos legais/validade | Decisões pendentes, mantidas fechadas nos comportamentos dependentes |
|
||||
|
||||
A decisão seguinte necessária é a autorização expressa de instalação privada do SHA escolhido, identificando se inclui os menus `3558647e`, mantendo publicação, NDA, notificações e agendamento nos estados acima e respeitando a janela exclusiva de Git. Esta verificação não concede essa autorização nem a solicita novamente para acções já executadas de backup/restauro.
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"state": "BLOCKED_BEFORE_INSTALLATION",
|
||||
"reason": "Explicit authorisation condition 4: executor follows main, which differs from the authorised revision.",
|
||||
"authorised_revision": "3558647e8d82092cd0e31eefbe8b9e5e9eacf573",
|
||||
"main_revision": "96f510965f70581a4b41dd94463f598b77dd7cf5",
|
||||
"target": {
|
||||
"configured_branch": "main",
|
||||
"installed_revision": "96f510965f70581a4b41dd94463f598b77dd7cf5",
|
||||
"deploy_service": "inactive",
|
||||
"deploy_path": "active",
|
||||
"pending_requests": 0,
|
||||
"backup_receipt_exists": true,
|
||||
"restore_verified": true,
|
||||
"installation_started": false
|
||||
},
|
||||
"installation_authorisation_received": true,
|
||||
"branch_update_performed": false,
|
||||
"request_created": false,
|
||||
"installation_started": false,
|
||||
"rollback_required": false,
|
||||
"migrations_performed": false,
|
||||
"email_sent": false,
|
||||
"operational_data_or_configuration_writes": false,
|
||||
"previous_backup_restore_evidence": "outputs/offers-preinstall-verification-20261114/qa-result.json",
|
||||
"possible_next_step": {
|
||||
"kind": "explicit decision to fast-forward main to authorised candidate; may trigger the deployment executor",
|
||||
"fast_forward_possible": true,
|
||||
"command": "git push origin 3558647e8d82092cd0e31eefbe8b9e5e9eacf573:refs/heads/main",
|
||||
"force": false,
|
||||
"other_local_documentation_commits_excluded": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"stage": "/var/backups/investmoz-offers-exact-install-3558647e-20261114/backup",
|
||||
"created_at": "2026-11-14T17:51:40Z",
|
||||
"target": {
|
||||
"database": "teste_investidor",
|
||||
"port": 5432
|
||||
},
|
||||
"tables": 219,
|
||||
"files_verified": 417,
|
||||
"dump_sha256": "88152dc39367f5e59eb072f63b44d3a949e93d73ae855eccb33c3c0b9e37bf6c",
|
||||
"files_sha256": "aa6248e0eec3d27d58a4bbb3c16848a70dcb3524e6cafc6340b682984a8bbdb3",
|
||||
"restore_verified": false
|
||||
}
|
||||
@@ -0,0 +1,663 @@
|
||||
{
|
||||
"path": {
|
||||
"funding_selections": [],
|
||||
"preparation_action": null,
|
||||
"private_investment": null,
|
||||
"case": {
|
||||
"id": "d51a3c8b-6210-4d2a-93ec-17ee61452e79",
|
||||
"title": "Synthetic acquisition acceptance",
|
||||
"version": 5,
|
||||
"closed": false
|
||||
},
|
||||
"journey_id": "3da63c95-34ac-4f6f-9f97-9c35c261cb17",
|
||||
"definition_id": "61000919-0000-4000-8000-000000000001",
|
||||
"definition_version": 1,
|
||||
"source_sha256": "aa13c53138ec15cd808de43d9dc2fc6808da86943d09d3352c9dd0944bdae364",
|
||||
"phases": {
|
||||
"1": "Ideia e viabilidade",
|
||||
"2": "Formaliza\u00e7\u00e3o e dossier",
|
||||
"3": "Licenciamento e terra",
|
||||
"4": "Financiamento",
|
||||
"5": "Execu\u00e7\u00e3o e acompanhamento"
|
||||
},
|
||||
"steps": [
|
||||
{
|
||||
"code": "S01",
|
||||
"phase": 1,
|
||||
"title": "Sistematizar a ideia",
|
||||
"branch": null,
|
||||
"sequence": 1,
|
||||
"dependencies": [],
|
||||
"source_paragraph": 22,
|
||||
"id": "ad1b09ac-2082-454c-9fc9-dec3e94d61f7",
|
||||
"version": 1,
|
||||
"state": "not_started",
|
||||
"saved_state": "not_started",
|
||||
"applicability": "applicable",
|
||||
"reason": "",
|
||||
"detail": {
|
||||
"state": "not_started"
|
||||
},
|
||||
"preparation_allowed": true,
|
||||
"action_gaps": [],
|
||||
"context_review_required": false
|
||||
},
|
||||
{
|
||||
"code": "S02",
|
||||
"phase": 1,
|
||||
"title": "An\u00e1lise preliminar",
|
||||
"branch": null,
|
||||
"sequence": 2,
|
||||
"dependencies": [],
|
||||
"source_paragraph": 23,
|
||||
"id": "1d17b451-b95a-4a29-a829-d75990f75961",
|
||||
"version": 1,
|
||||
"state": "not_started",
|
||||
"saved_state": "not_started",
|
||||
"applicability": "applicable",
|
||||
"reason": "",
|
||||
"detail": {
|
||||
"state": "not_started"
|
||||
},
|
||||
"preparation_allowed": true,
|
||||
"action_gaps": [],
|
||||
"context_review_required": true
|
||||
},
|
||||
{
|
||||
"code": "S03",
|
||||
"phase": 1,
|
||||
"title": "Decis\u00e3o sobre a viabilidade",
|
||||
"branch": null,
|
||||
"sequence": 3,
|
||||
"dependencies": [],
|
||||
"source_paragraph": 24,
|
||||
"id": "32d9cd0f-e854-4129-8358-ed17a8574d95",
|
||||
"version": 1,
|
||||
"state": "not_started",
|
||||
"saved_state": "not_started",
|
||||
"applicability": "applicable",
|
||||
"reason": "",
|
||||
"detail": {
|
||||
"state": "not_started"
|
||||
},
|
||||
"preparation_allowed": true,
|
||||
"action_gaps": [],
|
||||
"context_review_required": true
|
||||
},
|
||||
{
|
||||
"code": "S04",
|
||||
"phase": 1,
|
||||
"title": "Estruturar o investimento",
|
||||
"branch": null,
|
||||
"sequence": 4,
|
||||
"dependencies": [],
|
||||
"source_paragraph": 25,
|
||||
"id": "0a2005fe-2cdd-4d49-a8e9-052403c61095",
|
||||
"version": 1,
|
||||
"state": "not_started",
|
||||
"saved_state": "not_started",
|
||||
"applicability": "applicable",
|
||||
"reason": "",
|
||||
"detail": {
|
||||
"state": "not_started"
|
||||
},
|
||||
"preparation_allowed": true,
|
||||
"action_gaps": [],
|
||||
"context_review_required": true
|
||||
},
|
||||
{
|
||||
"code": "S05",
|
||||
"phase": 2,
|
||||
"title": "Constitui\u00e7\u00e3o e registo",
|
||||
"branch": null,
|
||||
"sequence": 5,
|
||||
"dependencies": [],
|
||||
"source_paragraph": 26,
|
||||
"id": "cf6c4c1b-61a0-4c7f-85c7-b6a21c7adbb4",
|
||||
"version": 1,
|
||||
"state": "not_started",
|
||||
"saved_state": "not_started",
|
||||
"applicability": "applicable",
|
||||
"reason": "",
|
||||
"detail": {
|
||||
"state": "not_started"
|
||||
},
|
||||
"preparation_allowed": true,
|
||||
"action_gaps": [],
|
||||
"context_review_required": false
|
||||
},
|
||||
{
|
||||
"code": "S06",
|
||||
"phase": 2,
|
||||
"title": "Preparar o dossier",
|
||||
"branch": null,
|
||||
"sequence": 6,
|
||||
"dependencies": [],
|
||||
"source_paragraph": 27,
|
||||
"id": "881cd5ed-81a1-48ce-8278-2d2cb568e27b",
|
||||
"version": 1,
|
||||
"state": "not_started",
|
||||
"saved_state": "not_started",
|
||||
"applicability": "applicable",
|
||||
"reason": "",
|
||||
"detail": {
|
||||
"state": "not_started"
|
||||
},
|
||||
"preparation_allowed": true,
|
||||
"action_gaps": [],
|
||||
"context_review_required": false
|
||||
},
|
||||
{
|
||||
"code": "S07",
|
||||
"phase": 2,
|
||||
"title": "Submeter quando aplic\u00e1vel",
|
||||
"branch": null,
|
||||
"sequence": 7,
|
||||
"dependencies": {
|
||||
"submit": [
|
||||
"S06"
|
||||
]
|
||||
},
|
||||
"source_paragraph": 28,
|
||||
"id": "0d57e596-26f3-4ec1-a8fd-aae1cf3aecce",
|
||||
"version": 1,
|
||||
"state": "not_started",
|
||||
"saved_state": "not_started",
|
||||
"applicability": "applicable",
|
||||
"reason": "",
|
||||
"detail": {
|
||||
"state": "not_started"
|
||||
},
|
||||
"preparation_allowed": true,
|
||||
"action_gaps": {
|
||||
"submit": [
|
||||
"S06"
|
||||
]
|
||||
},
|
||||
"context_review_required": false
|
||||
},
|
||||
{
|
||||
"code": "S08",
|
||||
"phase": 3,
|
||||
"title": "Licenciamento pr\u00e9vio",
|
||||
"branch": null,
|
||||
"sequence": 8,
|
||||
"dependencies": [],
|
||||
"source_paragraph": 29,
|
||||
"id": "51be4636-e771-4a90-8b02-a7e7e19799d2",
|
||||
"version": 1,
|
||||
"state": "not_started",
|
||||
"saved_state": "not_started",
|
||||
"applicability": "applicable",
|
||||
"reason": "",
|
||||
"detail": {
|
||||
"state": "not_started"
|
||||
},
|
||||
"preparation_allowed": true,
|
||||
"action_gaps": [],
|
||||
"context_review_required": true
|
||||
},
|
||||
{
|
||||
"code": "S09",
|
||||
"phase": 3,
|
||||
"title": "Necessidade de terra",
|
||||
"branch": null,
|
||||
"sequence": 9,
|
||||
"dependencies": [],
|
||||
"source_paragraph": 30,
|
||||
"id": "8860d60b-557a-4cae-9e9a-ffce052f0ac1",
|
||||
"version": 1,
|
||||
"state": "not_started",
|
||||
"saved_state": "not_started",
|
||||
"applicability": "applicable",
|
||||
"reason": "",
|
||||
"detail": {
|
||||
"state": "not_started"
|
||||
},
|
||||
"preparation_allowed": true,
|
||||
"action_gaps": [],
|
||||
"context_review_required": false
|
||||
},
|
||||
{
|
||||
"code": "S10",
|
||||
"phase": 3,
|
||||
"title": "Regulariza\u00e7\u00e3o da terra",
|
||||
"branch": "land_needed",
|
||||
"sequence": 10,
|
||||
"dependencies": [],
|
||||
"source_paragraph": 31,
|
||||
"id": "fefbb44c-0346-4309-a626-d64f89dc3ed8",
|
||||
"version": 1,
|
||||
"state": "not_started",
|
||||
"saved_state": "not_started",
|
||||
"applicability": "undetermined",
|
||||
"reason": "",
|
||||
"detail": {
|
||||
"state": "not_started"
|
||||
},
|
||||
"preparation_allowed": true,
|
||||
"action_gaps": [],
|
||||
"context_review_required": false
|
||||
},
|
||||
{
|
||||
"code": "S11",
|
||||
"phase": 3,
|
||||
"title": "Confirmar regulariza\u00e7\u00e3o aplic\u00e1vel",
|
||||
"branch": null,
|
||||
"sequence": 11,
|
||||
"dependencies": [],
|
||||
"source_paragraph": 32,
|
||||
"id": "c91ca8c7-8075-4ddc-b2dc-d7f1fd820b54",
|
||||
"version": 1,
|
||||
"state": "not_started",
|
||||
"saved_state": "not_started",
|
||||
"applicability": "applicable",
|
||||
"reason": "",
|
||||
"detail": {
|
||||
"state": "not_started"
|
||||
},
|
||||
"preparation_allowed": true,
|
||||
"action_gaps": [],
|
||||
"context_review_required": false
|
||||
},
|
||||
{
|
||||
"code": "S12",
|
||||
"phase": 4,
|
||||
"title": "Necessidade e estrutura de financiamento",
|
||||
"branch": null,
|
||||
"sequence": 12,
|
||||
"dependencies": [],
|
||||
"source_paragraph": 33,
|
||||
"id": "073bae02-69a6-463e-90b9-d85a3750c5c1",
|
||||
"version": 1,
|
||||
"state": "not_started",
|
||||
"saved_state": "not_started",
|
||||
"applicability": "applicable",
|
||||
"reason": "",
|
||||
"detail": {
|
||||
"state": "not_started"
|
||||
},
|
||||
"preparation_allowed": true,
|
||||
"action_gaps": [],
|
||||
"context_review_required": true
|
||||
},
|
||||
{
|
||||
"code": "S13",
|
||||
"phase": 4,
|
||||
"title": "Dilig\u00eancia e negocia\u00e7\u00e3o financeira",
|
||||
"branch": "external_funding",
|
||||
"sequence": 13,
|
||||
"dependencies": [],
|
||||
"source_paragraph": 34,
|
||||
"id": "9610ed18-2b00-42b7-ba45-109d786fbfab",
|
||||
"version": 1,
|
||||
"state": "not_started",
|
||||
"saved_state": "not_started",
|
||||
"applicability": "undetermined",
|
||||
"reason": "",
|
||||
"detail": {
|
||||
"state": "not_started"
|
||||
},
|
||||
"preparation_allowed": true,
|
||||
"action_gaps": [],
|
||||
"context_review_required": true
|
||||
},
|
||||
{
|
||||
"code": "S14",
|
||||
"phase": 4,
|
||||
"title": "Decis\u00e3o do financiador",
|
||||
"branch": "external_funding",
|
||||
"sequence": 14,
|
||||
"dependencies": [],
|
||||
"source_paragraph": 35,
|
||||
"id": "0d9db59f-8237-4541-a962-ef91d38431e2",
|
||||
"version": 1,
|
||||
"state": "not_started",
|
||||
"saved_state": "not_started",
|
||||
"applicability": "undetermined",
|
||||
"reason": "",
|
||||
"detail": {
|
||||
"state": "not_started"
|
||||
},
|
||||
"preparation_allowed": true,
|
||||
"action_gaps": [],
|
||||
"context_review_required": true
|
||||
},
|
||||
{
|
||||
"code": "S15",
|
||||
"phase": 4,
|
||||
"title": "Fecho e desembolso",
|
||||
"branch": "external_funding",
|
||||
"sequence": 15,
|
||||
"dependencies": {
|
||||
"execute": [
|
||||
"S14"
|
||||
]
|
||||
},
|
||||
"source_paragraph": 36,
|
||||
"id": "071057b5-cae7-4bfe-abec-749ff219f46a",
|
||||
"version": 1,
|
||||
"state": "not_started",
|
||||
"saved_state": "not_started",
|
||||
"applicability": "undetermined",
|
||||
"reason": "",
|
||||
"detail": {
|
||||
"state": "not_started"
|
||||
},
|
||||
"preparation_allowed": true,
|
||||
"action_gaps": {
|
||||
"execute": [
|
||||
"S14"
|
||||
]
|
||||
},
|
||||
"context_review_required": true
|
||||
},
|
||||
{
|
||||
"code": "S16",
|
||||
"phase": 5,
|
||||
"title": "Implementa\u00e7\u00e3o",
|
||||
"branch": null,
|
||||
"sequence": 16,
|
||||
"dependencies": [],
|
||||
"source_paragraph": 37,
|
||||
"id": "f6cdd881-7ceb-49e4-a092-c1b7677de0c9",
|
||||
"version": 1,
|
||||
"state": "not_started",
|
||||
"saved_state": "not_started",
|
||||
"applicability": "applicable",
|
||||
"reason": "",
|
||||
"detail": {
|
||||
"state": "not_started"
|
||||
},
|
||||
"preparation_allowed": true,
|
||||
"action_gaps": [],
|
||||
"context_review_required": false
|
||||
},
|
||||
{
|
||||
"code": "S17",
|
||||
"phase": 5,
|
||||
"title": "Inspec\u00e7\u00f5es, testes e licen\u00e7a de opera\u00e7\u00e3o",
|
||||
"branch": null,
|
||||
"sequence": 17,
|
||||
"dependencies": [],
|
||||
"source_paragraph": 38,
|
||||
"id": "53e9c8ea-3fe2-4945-92d9-c46d6a053b21",
|
||||
"version": 1,
|
||||
"state": "not_started",
|
||||
"saved_state": "not_started",
|
||||
"applicability": "applicable",
|
||||
"reason": "",
|
||||
"detail": {
|
||||
"state": "not_started"
|
||||
},
|
||||
"preparation_allowed": true,
|
||||
"action_gaps": [],
|
||||
"context_review_required": true
|
||||
},
|
||||
{
|
||||
"code": "S18",
|
||||
"phase": 5,
|
||||
"title": "In\u00edcio de opera\u00e7\u00e3o",
|
||||
"branch": null,
|
||||
"sequence": 18,
|
||||
"dependencies": {
|
||||
"operate": [
|
||||
"S17"
|
||||
]
|
||||
},
|
||||
"source_paragraph": 39,
|
||||
"id": "31728d61-5f97-41ce-ab31-17aa1c7712fe",
|
||||
"version": 1,
|
||||
"state": "not_started",
|
||||
"saved_state": "not_started",
|
||||
"applicability": "applicable",
|
||||
"reason": "",
|
||||
"detail": {
|
||||
"state": "not_started"
|
||||
},
|
||||
"preparation_allowed": true,
|
||||
"action_gaps": {
|
||||
"operate": [
|
||||
"S17"
|
||||
]
|
||||
},
|
||||
"context_review_required": false
|
||||
},
|
||||
{
|
||||
"code": "S19",
|
||||
"phase": 5,
|
||||
"title": "Acompanhamento recorrente",
|
||||
"branch": null,
|
||||
"sequence": 19,
|
||||
"dependencies": [],
|
||||
"source_paragraph": 40,
|
||||
"id": "c90e77d5-55ae-4e94-a0bb-4aeed7a418c6",
|
||||
"version": 1,
|
||||
"state": "not_started",
|
||||
"saved_state": "not_started",
|
||||
"applicability": "applicable",
|
||||
"reason": "",
|
||||
"detail": {
|
||||
"state": "not_started"
|
||||
},
|
||||
"preparation_allowed": true,
|
||||
"action_gaps": [],
|
||||
"context_review_required": false
|
||||
}
|
||||
],
|
||||
"facts": {
|
||||
"land_needed": "unknown",
|
||||
"external_funding": "unknown",
|
||||
"viability": "unknown"
|
||||
},
|
||||
"fact_reasons": [],
|
||||
"context_opportunity": null,
|
||||
"opportunity_options": [],
|
||||
"fintech": null,
|
||||
"canWrite": true,
|
||||
"canReview": false,
|
||||
"next": {
|
||||
"code": "S01",
|
||||
"title": "Sistematizar a ideia",
|
||||
"action": "Preparar",
|
||||
"href": "#S01"
|
||||
},
|
||||
"investment_context": {
|
||||
"opportunity_choices": [],
|
||||
"preparation_documents": [],
|
||||
"preparation_snapshot": null,
|
||||
"revision": 1,
|
||||
"selection": {
|
||||
"opportunity": {
|
||||
"key": "opportunity:28",
|
||||
"kind": "opportunity",
|
||||
"id": "28",
|
||||
"title": "Synthetic acquisition acceptance",
|
||||
"version": 5,
|
||||
"href": "\/minhas-oportunidades\/ofertas\/28",
|
||||
"fields": {
|
||||
"project_title": "Synthetic acquisition acceptance",
|
||||
"project_summary": "Existing company description to reuse once in investment preparation.",
|
||||
"province": "Synthetic province",
|
||||
"province_id": "855412e5-27b2-4e0f-90e2-c690e2698ae3",
|
||||
"location": "Synthetic province"
|
||||
},
|
||||
"documents": [],
|
||||
"visibility": "private",
|
||||
"state": "approved",
|
||||
"sha256": "54f87633f70b4b840a9b5ed445e8b437f7dd6c21097119b0324d79b0322dfce3"
|
||||
}
|
||||
},
|
||||
"fields": {
|
||||
"project_title": "Synthetic acquisition acceptance",
|
||||
"investment_regime": "general",
|
||||
"special_zone_id": "",
|
||||
"currency": "MZN",
|
||||
"project_summary": "Existing company description to reuse once in investment preparation.",
|
||||
"province": "Synthetic province",
|
||||
"province_id": "855412e5-27b2-4e0f-90e2-c690e2698ae3",
|
||||
"location": "Synthetic province"
|
||||
},
|
||||
"provenance": {
|
||||
"project_title": {
|
||||
"kind": "dossier",
|
||||
"id": "d51a3c8b-6210-4d2a-93ec-17ee61452e79",
|
||||
"version": 2
|
||||
},
|
||||
"currency": {
|
||||
"kind": "process_declaration",
|
||||
"actor_id": 126,
|
||||
"context_revision": 1
|
||||
},
|
||||
"project_summary": {
|
||||
"key": "opportunity:28",
|
||||
"kind": "opportunity",
|
||||
"id": "28",
|
||||
"title": "Synthetic acquisition acceptance",
|
||||
"version": 5,
|
||||
"href": "\/minhas-oportunidades\/ofertas\/28",
|
||||
"documents": [],
|
||||
"visibility": "private",
|
||||
"state": "approved",
|
||||
"sha256": "54f87633f70b4b840a9b5ed445e8b437f7dd6c21097119b0324d79b0322dfce3"
|
||||
},
|
||||
"province": {
|
||||
"key": "opportunity:28",
|
||||
"kind": "opportunity",
|
||||
"id": "28",
|
||||
"title": "Synthetic acquisition acceptance",
|
||||
"version": 5,
|
||||
"href": "\/minhas-oportunidades\/ofertas\/28",
|
||||
"documents": [],
|
||||
"visibility": "private",
|
||||
"state": "approved",
|
||||
"sha256": "54f87633f70b4b840a9b5ed445e8b437f7dd6c21097119b0324d79b0322dfce3"
|
||||
},
|
||||
"province_id": {
|
||||
"key": "opportunity:28",
|
||||
"kind": "opportunity",
|
||||
"id": "28",
|
||||
"title": "Synthetic acquisition acceptance",
|
||||
"version": 5,
|
||||
"href": "\/minhas-oportunidades\/ofertas\/28",
|
||||
"documents": [],
|
||||
"visibility": "private",
|
||||
"state": "approved",
|
||||
"sha256": "54f87633f70b4b840a9b5ed445e8b437f7dd6c21097119b0324d79b0322dfce3"
|
||||
},
|
||||
"location": {
|
||||
"key": "opportunity:28",
|
||||
"kind": "opportunity",
|
||||
"id": "28",
|
||||
"title": "Synthetic acquisition acceptance",
|
||||
"version": 5,
|
||||
"href": "\/minhas-oportunidades\/ofertas\/28",
|
||||
"documents": [],
|
||||
"visibility": "private",
|
||||
"state": "approved",
|
||||
"sha256": "54f87633f70b4b840a9b5ed445e8b437f7dd6c21097119b0324d79b0322dfce3"
|
||||
}
|
||||
},
|
||||
"review_steps": {
|
||||
"S02": {
|
||||
"revision": 1,
|
||||
"reason": "O contexto mudou; reveja os pressupostos e a evid\u00eancia aplic\u00e1vel."
|
||||
},
|
||||
"S03": {
|
||||
"revision": 1,
|
||||
"reason": "O contexto mudou; reveja os pressupostos e a evid\u00eancia aplic\u00e1vel."
|
||||
},
|
||||
"S04": {
|
||||
"revision": 1,
|
||||
"reason": "O contexto mudou; reveja os pressupostos e a evid\u00eancia aplic\u00e1vel."
|
||||
},
|
||||
"S08": {
|
||||
"revision": 1,
|
||||
"reason": "O contexto mudou; reveja os pressupostos e a evid\u00eancia aplic\u00e1vel."
|
||||
},
|
||||
"S12": {
|
||||
"revision": 1,
|
||||
"reason": "O contexto mudou; reveja os pressupostos e a evid\u00eancia aplic\u00e1vel."
|
||||
},
|
||||
"S13": {
|
||||
"revision": 1,
|
||||
"reason": "O contexto mudou; reveja os pressupostos e a evid\u00eancia aplic\u00e1vel."
|
||||
},
|
||||
"S14": {
|
||||
"revision": 1,
|
||||
"reason": "O contexto mudou; reveja os pressupostos e a evid\u00eancia aplic\u00e1vel."
|
||||
},
|
||||
"S15": {
|
||||
"revision": 1,
|
||||
"reason": "O contexto mudou; reveja os pressupostos e a evid\u00eancia aplic\u00e1vel."
|
||||
},
|
||||
"S17": {
|
||||
"revision": 1,
|
||||
"reason": "O contexto mudou; reveja os pressupostos e a evid\u00eancia aplic\u00e1vel."
|
||||
}
|
||||
},
|
||||
"dependency_hash": "7c4eca9a9377b1c42478becca10e833db253fe5abb566731a9d9bb0a6ba72baf",
|
||||
"updated_at": "2026-11-14T18:07:17+00:00",
|
||||
"updated_by": 126,
|
||||
"requirements_assessment": {
|
||||
"version": "context-requirements-20261023",
|
||||
"requirements": [],
|
||||
"fiscal_notice": "Localiza\u00e7\u00e3o, corredor, cluster ou projecto \u00e2ncora n\u00e3o activam benef\u00edcios fiscais. O simulador mant\u00e9m as condi\u00e7\u00f5es, fonte, per\u00edodo e \u00e2mbito pr\u00f3prios; condi\u00e7\u00f5es ausentes permanecem indeterminadas."
|
||||
},
|
||||
"complements": []
|
||||
},
|
||||
"results": [
|
||||
{
|
||||
"id": "2f494a79-71e0-4ed9-a2b1-51b6b60987c4",
|
||||
"source_key": "f840ebc37a03761ad285d6c22abfc1a5383f2ec87433c0690472420abd21baea",
|
||||
"draft_id": "b1c4e61e-1d8e-49bc-9f12-7bc1fea93724",
|
||||
"draft_version": 1,
|
||||
"snapshot_sha256": "47f89ac2f2882e4adc27a74e2cd0a986f612b0779f2f5f6a7774d3734ba70ea2",
|
||||
"financial_snapshot": "{\"version\":1,\"data\":{\"budgetPeriod\":\"Mensal\",\"loan\":{\"principal\":\"100000\",\"rate\":\"0\",\"months\":\"24\",\"rateType\":\"nominal\",\"periodMonths\":\"1\",\"gracePeriods\":\"0\",\"upfrontFee\":\"0\",\"recurringFee\":\"0\"},\"breakEven\":{\"fixed\":\"3000\",\"price\":\"100\",\"variable\":\"50\"},\"funding\":{\"total\":\"100000\",\"secured\":\"30000\"},\"incomes\":[{\"label\":\"Synthetic scenario revenue\",\"amount\":\"10000\"}],\"expenses\":[{\"label\":\"Declared rent at synthetic province\",\"amount\":\"2000\"},{\"label\":\"Declared transport at synthetic province\",\"amount\":\"1000\"}]}}",
|
||||
"context_opportunity": null,
|
||||
"currency": "MZN",
|
||||
"engine": "financial-calculators:v1",
|
||||
"kind": "local_calculation_inputs",
|
||||
"context_state": "current",
|
||||
"context_revision": 1,
|
||||
"context_dependency_hash": "7c4eca9a9377b1c42478becca10e833db253fe5abb566731a9d9bb0a6ba72baf",
|
||||
"recorded_at": "2026-11-14T18:07:20+00:00"
|
||||
}
|
||||
],
|
||||
"requests": [],
|
||||
"documents": [],
|
||||
"fiscal_reports": [],
|
||||
"land_report": null,
|
||||
"preparation_report": null,
|
||||
"company_reports": [],
|
||||
"zone_contexts": [],
|
||||
"sector_contexts": [],
|
||||
"sector_references": [],
|
||||
"content_sources": [],
|
||||
"tasks": [],
|
||||
"services": [],
|
||||
"tools": {
|
||||
"funding": "\/fundos?kind=institution&location_mode=physical&context=d51a3c8b-6210-4d2a-93ec-17ee61452e79",
|
||||
"simulator": "\/simulador-fiscal?context=d51a3c8b-6210-4d2a-93ec-17ee61452e79&draft=b1c4e61e-1d8e-49bc-9f12-7bc1fea93724",
|
||||
"preparation": null,
|
||||
"company": "\/investimentos\/d51a3c8b-6210-4d2a-93ec-17ee61452e79\/forma-juridica",
|
||||
"desk": "\/balcao-virtual\/dossiers\/d51a3c8b-6210-4d2a-93ec-17ee61452e79",
|
||||
"guidance": "\/balcao-virtual\/dossiers\/d51a3c8b-6210-4d2a-93ec-17ee61452e79\/orientacao",
|
||||
"library": "\/documentos?context=d51a3c8b-6210-4d2a-93ec-17ee61452e79",
|
||||
"opportunities": "\/oportunidades?context=d51a3c8b-6210-4d2a-93ec-17ee61452e79",
|
||||
"territory": "\/zonas?context=d51a3c8b-6210-4d2a-93ec-17ee61452e79",
|
||||
"partners": "\/parcerias\/painel?context=d51a3c8b-6210-4d2a-93ec-17ee61452e79",
|
||||
"land": "\/investidor\/terra-duat\/novo?context=d51a3c8b-6210-4d2a-93ec-17ee61452e79"
|
||||
}
|
||||
},
|
||||
"saved": {
|
||||
"project_title": "Synthetic acquisition acceptance",
|
||||
"currency": "MZN",
|
||||
"project_summary": "Existing company description to reuse once in investment preparation.",
|
||||
"province_id": "855412e5-27b2-4e0f-90e2-c690e2698ae3",
|
||||
"journey_type": "investor",
|
||||
"purpose": "simulate_only",
|
||||
"context_applicability_snapshot": "{\"version\":1,\"context_revision\":1,\"consumer_opportunity\":null,\"portfolio_origin\":null,\"zone\":null,\"provenance\":{\"project_title\":{\"kind\":\"dossier\",\"id\":\"d51a3c8b-6210-4d2a-93ec-17ee61452e79\",\"version\":2},\"currency\":{\"kind\":\"process_declaration\",\"actor_id\":126,\"context_revision\":1},\"project_summary\":{\"key\":\"opportunity:28\",\"kind\":\"opportunity\",\"id\":\"28\",\"title\":\"Synthetic acquisition acceptance\",\"version\":5,\"href\":\"\\\/minhas-oportunidades\\\/ofertas\\\/28\",\"documents\":[],\"visibility\":\"private\",\"state\":\"approved\",\"sha256\":\"54f87633f70b4b840a9b5ed445e8b437f7dd6c21097119b0324d79b0322dfce3\"},\"province\":{\"key\":\"opportunity:28\",\"kind\":\"opportunity\",\"id\":\"28\",\"title\":\"Synthetic acquisition acceptance\",\"version\":5,\"href\":\"\\\/minhas-oportunidades\\\/ofertas\\\/28\",\"documents\":[],\"visibility\":\"private\",\"state\":\"approved\",\"sha256\":\"54f87633f70b4b840a9b5ed445e8b437f7dd6c21097119b0324d79b0322dfce3\"},\"province_id\":{\"key\":\"opportunity:28\",\"kind\":\"opportunity\",\"id\":\"28\",\"title\":\"Synthetic acquisition acceptance\",\"version\":5,\"href\":\"\\\/minhas-oportunidades\\\/ofertas\\\/28\",\"documents\":[],\"visibility\":\"private\",\"state\":\"approved\",\"sha256\":\"54f87633f70b4b840a9b5ed445e8b437f7dd6c21097119b0324d79b0322dfce3\"},\"location\":{\"key\":\"opportunity:28\",\"kind\":\"opportunity\",\"id\":\"28\",\"title\":\"Synthetic acquisition acceptance\",\"version\":5,\"href\":\"\\\/minhas-oportunidades\\\/ofertas\\\/28\",\"documents\":[],\"visibility\":\"private\",\"state\":\"approved\",\"sha256\":\"54f87633f70b4b840a9b5ed445e8b437f7dd6c21097119b0324d79b0322dfce3\"}},\"opportunity_choices\":[],\"complements\":[],\"requirements_assessment\":{\"version\":\"context-requirements-20261023\",\"requirements\":[],\"fiscal_notice\":\"Localiza\u00e7\u00e3o, corredor, cluster ou projecto \u00e2ncora n\u00e3o activam benef\u00edcios fiscais. O simulador mant\u00e9m as condi\u00e7\u00f5es, fonte, per\u00edodo e \u00e2mbito pr\u00f3prios; condi\u00e7\u00f5es ausentes permanecem indeterminadas.\"},\"path\":{\"definition_id\":\"61000919-0000-4000-8000-000000000001\",\"definition_version\":1,\"source_sha256\":\"aa13c53138ec15cd808de43d9dc2fc6808da86943d09d3352c9dd0944bdae364\",\"steps\":[{\"code\":\"S10\",\"title\":\"Regulariza\u00e7\u00e3o da terra\",\"state\":\"not_started\",\"applicability\":\"undetermined\",\"reason\":\"\",\"context_review_required\":false},{\"code\":\"S13\",\"title\":\"Dilig\u00eancia e negocia\u00e7\u00e3o financeira\",\"state\":\"not_started\",\"applicability\":\"undetermined\",\"reason\":\"\",\"context_review_required\":true},{\"code\":\"S14\",\"title\":\"Decis\u00e3o do financiador\",\"state\":\"not_started\",\"applicability\":\"undetermined\",\"reason\":\"\",\"context_review_required\":true},{\"code\":\"S15\",\"title\":\"Fecho e desembolso\",\"state\":\"not_started\",\"applicability\":\"undetermined\",\"reason\":\"\",\"context_review_required\":true}]},\"land_preparation\":{\"need\":\"unknown\",\"reason\":\"\"},\"field_provenance\":{\"project_title\":{\"kind\":\"dossier\",\"version\":2},\"currency\":{\"kind\":\"process_declaration\",\"context_revision\":1},\"project_summary\":{\"kind\":\"opportunity\",\"title\":\"Synthetic acquisition acceptance\",\"version\":5},\"province\":{\"kind\":\"opportunity\",\"title\":\"Synthetic acquisition acceptance\",\"version\":5},\"province_id\":{\"kind\":\"opportunity\",\"title\":\"Synthetic acquisition acceptance\",\"version\":5},\"location\":{\"kind\":\"opportunity\",\"title\":\"Synthetic acquisition acceptance\",\"version\":5}},\"preparation\":null,\"fields\":{\"investment_regime\":\"general\",\"currency\":\"MZN\"},\"notice\":\"Refer\u00eancias e declara\u00e7\u00f5es para prepara\u00e7\u00e3o; sem reconhecimento autom\u00e1tico de elegibilidade ou benef\u00edcio.\"}",
|
||||
"financial_tools_snapshot": "{\"version\":1,\"data\":{\"budgetPeriod\":\"Mensal\",\"loan\":{\"principal\":\"100000\",\"rate\":\"0\",\"months\":\"24\",\"rateType\":\"nominal\",\"periodMonths\":\"1\",\"gracePeriods\":\"0\",\"upfrontFee\":\"0\",\"recurringFee\":\"0\"},\"breakEven\":{\"fixed\":\"3000\",\"price\":\"100\",\"variable\":\"50\"},\"funding\":{\"total\":\"100000\",\"secured\":\"30000\"},\"incomes\":[{\"label\":\"Synthetic scenario revenue\",\"amount\":\"10000\"}],\"expenses\":[{\"label\":\"Declared rent at synthetic province\",\"amount\":\"2000\"},{\"label\":\"Declared transport at synthetic province\",\"amount\":\"1000\"}]}}"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"tool":"phpstan","result":"passed","errors":0}
|
||||
@@ -0,0 +1,324 @@
|
||||
|
||||
> build
|
||||
> vp build
|
||||
|
||||
[plugin @laravel/vite-plugin-wayfinder] Types generated for actions, routes, form variants
|
||||
npm.cmd : [plugin laravel:fonts] Optimized font fallbacks require the optional "fontaine" package. Install it, or set
|
||||
"optimizedFallbacks: false" on your fonts to disable the feature.
|
||||
At line:9 char:444
|
||||
+ ... _BUILD='0'; npm.cmd run build *> outputs/offers-exact-install-2026111 ...
|
||||
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
+ CategoryInfo : NotSpecified: ([plugin laravel...le the feature.:String) [], RemoteException
|
||||
+ FullyQualifiedErrorId : NativeCommandError
|
||||
|
||||
transforming...
|
||||
✓ 2849 modules transformed.
|
||||
rendering chunks...
|
||||
computing gzip size...
|
||||
public/build/assets/layers-BWBAp2CZ.png 0.69 kB
|
||||
public/build/assets/layers-2x-Bpkbi35X.png 1.25 kB
|
||||
public/build/assets/marker-icon-hN30_KVU.png 1.46 kB
|
||||
public/build/fonts-manifest.json 5.74 kB │ gzip: 0.71 kB
|
||||
public/build/assets/instrument-sans-400-normal-DRC__1Mx.woff2 16.86 kB
|
||||
public/build/assets/instrument-sans-500-normal-Dk9ku72i.woff2 17.23 kB
|
||||
public/build/assets/instrument-sans-600-normal-B7fBEWYG.woff2 17.40 kB
|
||||
public/build/assets/instrument-sans-400-normal-D1W7dsQl.woff 21.24 kB
|
||||
public/build/assets/instrument-sans-500-normal-Z6ESRlEs.woff 21.65 kB
|
||||
public/build/assets/instrument-sans-600-normal-B9e8oLYv.woff 21.67 kB
|
||||
public/build/manifest.json 114.69 kB │ gzip: 12.01 kB
|
||||
public/build/assets/fonts-C9MNnjVw.css 2.35 kB │ gzip: 0.38 kB
|
||||
public/build/assets/investment-catalogue-WaRR19FV.css 8.15 kB │ gzip: 2.00 kB
|
||||
public/build/assets/interactive-investment-map-1DTFk8zz.css 10.57 kB │ gzip: 2.63 kB
|
||||
public/build/assets/app-UznX-FqS.css 157.42 kB │ gzip: 25.28 kB
|
||||
public/build/assets/input-text-BlEFHnL-.js 0.09 kB │ gzip: 0.10 kB
|
||||
public/build/assets/check-PcBNn_NG.js 0.12 kB │ gzip: 0.14 kB
|
||||
public/build/assets/company-comparison-qLTgpXjz.js 0.12 kB │ gzip: 0.13 kB
|
||||
public/build/assets/chevron-down-Bjkqe1aH.js 0.12 kB │ gzip: 0.14 kB
|
||||
public/build/assets/chevron-right-K0fOEtTQ.js 0.12 kB │ gzip: 0.14 kB
|
||||
public/build/assets/display-value-DRR1EAAK.js 0.13 kB │ gzip: 0.12 kB
|
||||
public/build/assets/value-text-BztaYdGl.js 0.14 kB │ gzip: 0.12 kB
|
||||
public/build/assets/loader-circle-DS8Tf4er.js 0.14 kB │ gzip: 0.15 kB
|
||||
public/build/assets/shared-B-QpuOU2.js 0.15 kB │ gzip: 0.13 kB
|
||||
public/build/assets/arrow-left-DMVHIw8Z.js 0.16 kB │ gzip: 0.16 kB
|
||||
public/build/assets/arrow-up-right-BIVouzdL.js 0.16 kB │ gzip: 0.15 kB
|
||||
public/build/assets/search-BRvPeo47.js 0.17 kB │ gzip: 0.16 kB
|
||||
public/build/assets/circle-check-DckzuBoW.js 0.17 kB │ gzip: 0.16 kB
|
||||
public/build/assets/product-usage-CHhCL93_.js 0.18 kB │ gzip: 0.17 kB
|
||||
public/build/assets/editorial-states-Bc7NQzsj.js 0.19 kB │ gzip: 0.16 kB
|
||||
public/build/assets/types-BoD_IJFv.js 0.19 kB │ gzip: 0.16 kB
|
||||
public/build/assets/participation-identity-BWabb4yN.js 0.24 kB │ gzip: 0.18 kB
|
||||
public/build/assets/circle-alert-uV7omYjr.js 0.24 kB │ gzip: 0.18 kB
|
||||
public/build/assets/eye-CmcEBfxN.js 0.25 kB │ gzip: 0.20 kB
|
||||
public/build/assets/log-in-Wgc8VLfq.js 0.25 kB │ gzip: 0.22 kB
|
||||
public/build/assets/partnership-history-CfclSLAo.js 0.27 kB │ gzip: 0.19 kB
|
||||
public/build/assets/compiler-runtime-sKsOXjDo.js 0.27 kB │ gzip: 0.23 kB
|
||||
public/build/assets/file-text-CEJtuNfp.js 0.33 kB │ gzip: 0.21 kB
|
||||
public/build/assets/confirm-Culw9Q-x.js 0.36 kB │ gzip: 0.22 kB
|
||||
public/build/assets/text-link-BBvcP60s.js 0.40 kB │ gzip: 0.27 kB
|
||||
public/build/assets/content-management-entry-C_oY4VyW.js 0.42 kB │ gzip: 0.31 kB
|
||||
public/build/assets/handshake-YqyTTm3a.js 0.44 kB │ gzip: 0.28 kB
|
||||
public/build/assets/investment-guides-DO58L8zr.js 0.45 kB │ gzip: 0.26 kB
|
||||
public/build/assets/catalogue-context-1vQ0-xK7.js 0.47 kB │ gzip: 0.30 kB
|
||||
public/build/assets/operation-key-Dr4VtGxa.js 0.51 kB │ gzip: 0.35 kB
|
||||
public/build/assets/spinner-Dmx1j41V.js 0.52 kB │ gzip: 0.36 kB
|
||||
public/build/assets/types-Cxp_Nskt.js 0.55 kB │ gzip: 0.32 kB
|
||||
public/build/assets/sliders-horizontal-D-4FqAHA.js 0.58 kB │ gzip: 0.26 kB
|
||||
public/build/assets/page-preparation-guard-B-OckhBt.js 0.59 kB │ gzip: 0.35 kB
|
||||
public/build/assets/portal-presentation-fH7FShU8.js 0.62 kB │ gzip: 0.38 kB
|
||||
public/build/assets/confidentiality-C2eqbRBb.js 0.64 kB │ gzip: 0.41 kB
|
||||
public/build/assets/dist-RzQD0wUd.js 0.68 kB │ gzip: 0.43 kB
|
||||
public/build/assets/dist-qw3FPjRO.js 0.74 kB │ gzip: 0.49 kB
|
||||
public/build/assets/labels-DxqjesIG.js 0.74 kB │ gzip: 0.46 kB
|
||||
public/build/assets/wayfinder-Dp7PqxvV.js 0.89 kB │ gzip: 0.53 kB
|
||||
public/build/assets/createLucideIcon-oMeAlXbs.js 0.95 kB │ gzip: 0.56 kB
|
||||
public/build/assets/use-interest-draft-waHUBAew.js 0.99 kB │ gzip: 0.51 kB
|
||||
public/build/assets/context-documents-Bh7vrWBo.js 1.01 kB │ gzip: 0.57 kB
|
||||
public/build/assets/label-Dpfg_IeY.js 1.02 kB │ gzip: 0.62 kB
|
||||
public/build/assets/technical-incident-labels-DlL6bSeE.js 1.02 kB │ gzip: 0.51 kB
|
||||
public/build/assets/investment-consumer-identity-Cxp_hGfX.js 1.07 kB │ gzip: 0.59 kB
|
||||
public/build/assets/article-Bp95nIri.js 1.08 kB │ gzip: 0.65 kB
|
||||
public/build/assets/search-state-RHLy6yI7.js 1.10 kB │ gzip: 0.68 kB
|
||||
public/build/assets/rolldown-runtime-Dd_uD5pT.js 1.10 kB │ gzip: 0.62 kB
|
||||
public/build/assets/catalogue-investment-map-C5u-ZS7q.js 1.12 kB │ gzip: 0.62 kB
|
||||
public/build/assets/investment-contexts-BQGiwu9n.js 1.17 kB │ gzip: 0.60 kB
|
||||
public/build/assets/profile-json-Bza-VQzw.js 1.20 kB │ gzip: 0.72 kB
|
||||
public/build/assets/document-COw_wOdO.js 1.23 kB │ gzip: 0.72 kB
|
||||
public/build/assets/shared-JTxA6RNb.js 1.24 kB │ gzip: 0.74 kB
|
||||
public/build/assets/public-detail-3AU70Dv-.js 1.25 kB │ gzip: 0.72 kB
|
||||
public/build/assets/demographic-context-BMdtlxjJ.js 1.27 kB │ gzip: 0.73 kB
|
||||
public/build/assets/public-offer-card-BDRgf7-u.js 1.30 kB │ gzip: 0.70 kB
|
||||
public/build/assets/input-BJ-Jg-jz.js 1.37 kB │ gzip: 0.75 kB
|
||||
public/build/assets/cluster-competitiveness-hNV0FaaW.js 1.37 kB │ gzip: 0.73 kB
|
||||
public/build/assets/verify-email-DLyeS-ky.js 1.40 kB │ gzip: 0.74 kB
|
||||
public/build/assets/business-types-SGCXhH6X.js 1.47 kB │ gzip: 0.75 kB
|
||||
public/build/assets/verification-BOcn3HIw.js 1.48 kB │ gzip: 0.52 kB
|
||||
public/build/assets/country-select-4uP5BvMf.js 1.49 kB │ gzip: 0.78 kB
|
||||
public/build/assets/territorial-source-evidence-eKB4ZB36.js 1.53 kB │ gzip: 0.83 kB
|
||||
public/build/assets/manage-rHtSD5LS.js 1.56 kB │ gzip: 0.74 kB
|
||||
public/build/assets/educational-article-card-0rCqZ1kX.js 1.59 kB │ gzip: 0.72 kB
|
||||
public/build/assets/partnership-source-links-VfrXk2FZ.js 1.65 kB │ gzip: 0.91 kB
|
||||
public/build/assets/catalogue-BtBt9fvW.js 1.78 kB │ gzip: 0.96 kB
|
||||
public/build/assets/technical-incident-notice-CjziWA6m.js 1.79 kB │ gzip: 0.89 kB
|
||||
public/build/assets/report-DRxtlCFu.js 1.80 kB │ gzip: 0.93 kB
|
||||
public/build/assets/preparation-index-CdgPdpQ-.js 1.87 kB │ gzip: 0.89 kB
|
||||
public/build/assets/opportunity-technical-incidents-C4aVbut7.js 1.88 kB │ gzip: 0.98 kB
|
||||
public/build/assets/password-input-DdsNob7W.js 1.89 kB │ gzip: 1.06 kB
|
||||
public/build/assets/passkey-verify-Xyqvu_s4.js 2.04 kB │ gzip: 1.15 kB
|
||||
public/build/assets/public-record-offline-BmjvnSLE.js 2.07 kB │ gzip: 1.22 kB
|
||||
public/build/assets/investors-Di6y94fY.js 2.09 kB │ gzip: 1.15 kB
|
||||
public/build/assets/forgot-password-yYaDq9Uu.js 2.10 kB │ gzip: 0.98 kB
|
||||
public/build/assets/sources-HCmnzcAX.js 2.13 kB │ gzip: 1.12 kB
|
||||
public/build/assets/saved-wal8Mb1T.js 2.21 kB │ gzip: 1.11 kB
|
||||
public/build/assets/zone-map-oV9KEwxY.js 2.26 kB │ gzip: 1.16 kB
|
||||
public/build/assets/appearance-BP-b3aFc.js 2.36 kB │ gzip: 1.14 kB
|
||||
public/build/assets/shared-CBJ3c_x-.js 2.37 kB │ gzip: 1.24 kB
|
||||
public/build/assets/editorial-subjects-C6_MfRSK.js 2.38 kB │ gzip: 1.14 kB
|
||||
public/build/assets/context-installation-D9SIQHPi.js 2.39 kB │ gzip: 1.23 kB
|
||||
public/build/assets/workspace-4_R1CLYI.js 2.40 kB │ gzip: 1.20 kB
|
||||
public/build/assets/reset-password-D-TXit1d.js 2.43 kB │ gzip: 0.98 kB
|
||||
public/build/assets/public-document-content-BHvs9J7S.js 2.47 kB │ gzip: 0.80 kB
|
||||
public/build/assets/manage-h1Vaw3Xs.js 2.47 kB │ gzip: 1.10 kB
|
||||
public/build/assets/confirm-password-Bz70GAZg.js 2.47 kB │ gzip: 1.06 kB
|
||||
public/build/assets/structures-Dr2nXdub.js 2.49 kB │ gzip: 1.35 kB
|
||||
public/build/assets/bootstrap-confirmation-9RmA5-Yz.js 2.55 kB │ gzip: 1.19 kB
|
||||
public/build/assets/territory-associations-ODVIHiZU.js 2.55 kB │ gzip: 1.11 kB
|
||||
public/build/assets/installation-Dly8Xg6e.js 2.59 kB │ gzip: 1.28 kB
|
||||
public/build/assets/technical-support-DlW3Jv7q.js 2.60 kB │ gzip: 1.18 kB
|
||||
public/build/assets/management-CvdeRZYq.js 2.68 kB │ gzip: 1.18 kB
|
||||
public/build/assets/experiments-DitFKzM4.js 2.73 kB │ gzip: 1.31 kB
|
||||
public/build/assets/password-CB_NxgQN.js 2.73 kB │ gzip: 0.73 kB
|
||||
public/build/assets/opportunity-install-B232n6lM.js 2.75 kB │ gzip: 1.34 kB
|
||||
public/build/assets/dist-BobFCjoG.js 2.92 kB │ gzip: 1.29 kB
|
||||
public/build/assets/offer-navigation-DLl-_mMp.js 2.98 kB │ gzip: 1.11 kB
|
||||
public/build/assets/portal-api-CpQBRfcu.js 2.98 kB │ gzip: 1.50 kB
|
||||
public/build/assets/two-factor-challenge-DXHJ-XjZ.js 3.24 kB │ gzip: 1.52 kB
|
||||
public/build/assets/territory-geometry-preview-TlklAYvq.js 3.25 kB │ gzip: 1.47 kB
|
||||
public/build/assets/workspace-D6eSwMPE.js 3.29 kB │ gzip: 1.44 kB
|
||||
public/build/assets/partnership-agreement-instrument-CCnuZNBy.js 3.43 kB │ gzip: 1.49 kB
|
||||
public/build/assets/funding-classification-DxK67Gr_.js 3.44 kB │ gzip: 1.64 kB
|
||||
public/build/assets/guide-mndEuKFT.js 3.45 kB │ gzip: 1.43 kB
|
||||
public/build/assets/history-DSPDM8gM.js 3.46 kB │ gzip: 1.70 kB
|
||||
public/build/assets/audit-Xdo8qR4z.js 3.51 kB │ gzip: 1.69 kB
|
||||
public/build/assets/show-D0mDuHqC.js 3.60 kB │ gzip: 1.82 kB
|
||||
public/build/assets/demography-D_JHlax_.js 3.70 kB │ gzip: 1.79 kB
|
||||
public/build/assets/corridor-agribusiness-BcIVf-hW.js 3.72 kB │ gzip: 1.53 kB
|
||||
public/build/assets/business-opportunity-CNz7NAr0.js 3.73 kB │ gzip: 1.63 kB
|
||||
public/build/assets/management-access-D4VE9LUB.js 3.75 kB │ gzip: 1.43 kB
|
||||
public/build/assets/businesses-QQGbugDY.js 3.76 kB │ gzip: 1.57 kB
|
||||
public/build/assets/editorial-reading-DUFQmJtD.js 3.89 kB │ gzip: 1.68 kB
|
||||
public/build/assets/catalog-B6dpdVsW.js 3.95 kB │ gzip: 1.51 kB
|
||||
public/build/assets/desk-assignments-CfWpiIpK.js 3.99 kB │ gzip: 1.95 kB
|
||||
public/build/assets/dashboard-CxoTLkSg.js 3.99 kB │ gzip: 1.61 kB
|
||||
public/build/assets/investment-context-cards-gJe_oyxw.js 4.05 kB │ gzip: 1.84 kB
|
||||
public/build/assets/shared-B4FWNTYh.js 4.09 kB │ gzip: 2.05 kB
|
||||
public/build/assets/opportunity-contact-form-Cb6iDQ1U.js 4.10 kB │ gzip: 1.67 kB
|
||||
public/build/assets/demographic-evidence-DGwOnf5g.js 4.38 kB │ gzip: 1.84 kB
|
||||
public/build/assets/conversation-BxjnUMLV.js 4.42 kB │ gzip: 1.95 kB
|
||||
public/build/assets/portfolio-5GQ_KZiC.js 4.45 kB │ gzip: 1.74 kB
|
||||
public/build/assets/reference-DUYC0k47.js 4.46 kB │ gzip: 2.00 kB
|
||||
public/build/assets/territory-geometry-import-BnSl5jzG.js 4.49 kB │ gzip: 1.90 kB
|
||||
public/build/assets/opportunity-origin-D3o6OTRg.js 4.64 kB │ gzip: 2.07 kB
|
||||
public/build/assets/context-inheritance-dd0VgP8h.js 4.64 kB │ gzip: 2.00 kB
|
||||
public/build/assets/installation-CnslW5oi.js 4.67 kB │ gzip: 1.95 kB
|
||||
public/build/assets/opportunities-YsMGMCR_.js 4.73 kB │ gzip: 1.77 kB
|
||||
public/build/assets/references-Dpnc1rzq.js 4.79 kB │ gzip: 2.22 kB
|
||||
public/build/assets/offer-confidential-access-Bu9EgZ3d.js 4.83 kB │ gzip: 1.91 kB
|
||||
public/build/assets/opportunity-curation-7Gt2cWer.js 4.88 kB │ gzip: 2.04 kB
|
||||
public/build/assets/business-verifications-CrRulyQa.js 4.90 kB │ gzip: 1.72 kB
|
||||
public/build/assets/crm-edit-comparison-Db53T7I7.js 4.91 kB │ gzip: 1.99 kB
|
||||
public/build/assets/investment-complements-CqmFjvBN.js 5.00 kB │ gzip: 2.05 kB
|
||||
public/build/assets/service-CbUB6_Ll.js 5.05 kB │ gzip: 2.33 kB
|
||||
public/build/assets/agreement-history-BfSa8E7Q.js 5.13 kB │ gzip: 2.10 kB
|
||||
public/build/assets/catalogue-DY6Wy5Cg.js 5.13 kB │ gzip: 2.54 kB
|
||||
public/build/assets/workspace-BeyUwWtD.js 5.20 kB │ gzip: 2.18 kB
|
||||
public/build/assets/article-editor-ByLxbPbJ.js 5.25 kB │ gzip: 2.20 kB
|
||||
public/build/assets/context-start-hE9CWWCV.js 5.46 kB │ gzip: 2.10 kB
|
||||
public/build/assets/economic-references-C7dNVUC6.js 5.61 kB │ gzip: 2.52 kB
|
||||
public/build/assets/convert-B3LtI2n6.js 5.81 kB │ gzip: 2.40 kB
|
||||
public/build/assets/editorial-subject-form-B2W8E2NJ.js 5.87 kB │ gzip: 2.66 kB
|
||||
public/build/assets/public-sector-cards-Esb1aTm6.js 5.94 kB │ gzip: 2.55 kB
|
||||
public/build/assets/demography-install-B57rikBv.js 6.05 kB │ gzip: 2.50 kB
|
||||
public/build/assets/context-to-HP9u9.js 6.06 kB │ gzip: 2.65 kB
|
||||
public/build/assets/request-BUGHIRJq.js 6.10 kB │ gzip: 2.41 kB
|
||||
public/build/assets/structure-geography-lV-3bl6R.js 6.12 kB │ gzip: 2.22 kB
|
||||
public/build/assets/editorial-workflow-DYYZzXOw.js 6.22 kB │ gzip: 2.29 kB
|
||||
public/build/assets/investment-map-panel-BXYumzxQ.js 6.27 kB │ gzip: 2.88 kB
|
||||
public/build/assets/procurement-B_QkirDu.js 6.35 kB │ gzip: 2.73 kB
|
||||
public/build/assets/register-CSFi8mO-.js 6.43 kB │ gzip: 2.71 kB
|
||||
public/build/assets/funding-BPHyNawC.js 6.49 kB │ gzip: 2.00 kB
|
||||
public/build/assets/leads-Bu2UK8ON.js 6.56 kB │ gzip: 2.82 kB
|
||||
public/build/assets/business-marketplace-DfUAwAmq.js 6.59 kB │ gzip: 2.51 kB
|
||||
public/build/assets/interest-xqtiIFPC.js 6.63 kB │ gzip: 2.23 kB
|
||||
public/build/assets/demography-BxaBdOBx.js 6.66 kB │ gzip: 2.51 kB
|
||||
public/build/assets/fiscal-qa-review-CEzw2OQP.js 6.75 kB │ gzip: 2.70 kB
|
||||
public/build/assets/business-interest-fAZE_Pbt.js 6.82 kB │ gzip: 2.60 kB
|
||||
public/build/assets/interests-BcpPowrJ.js 6.83 kB │ gzip: 2.78 kB
|
||||
public/build/assets/catalogue-j9LhZ6Oa.js 6.84 kB │ gzip: 2.82 kB
|
||||
public/build/assets/company-comparison-BwxGUPs3.js 6.92 kB │ gzip: 2.60 kB
|
||||
public/build/assets/business-classification-Bu0hQkg7.js 7.01 kB │ gzip: 2.57 kB
|
||||
public/build/assets/dist-Ddn_hEUd.js 7.02 kB │ gzip: 2.71 kB
|
||||
public/build/assets/blank-2jFZEe2q.js 7.07 kB │ gzip: 2.77 kB
|
||||
public/build/assets/analysis--82QOZav.js 7.11 kB │ gzip: 2.96 kB
|
||||
public/build/assets/profile-DWCNp6hP.js 7.19 kB │ gzip: 2.47 kB
|
||||
public/build/assets/install-DfaXd1NZ.js 7.23 kB │ gzip: 3.03 kB
|
||||
public/build/assets/investor-COYuuRY_.js 7.30 kB │ gzip: 2.89 kB
|
||||
public/build/assets/investor-path-CSExMjAt.js 7.36 kB │ gzip: 3.02 kB
|
||||
public/build/assets/proactive-supply-review-Bspf5LP7.js 7.55 kB │ gzip: 2.73 kB
|
||||
public/build/assets/legacy-Cgz97_0u.js 7.67 kB │ gzip: 2.56 kB
|
||||
public/build/assets/jsx-runtime-BpzPEenQ.js 7.89 kB │ gzip: 2.99 kB
|
||||
public/build/assets/shared-BFCbvJo7.js 7.90 kB │ gzip: 3.08 kB
|
||||
public/build/assets/business-objects-DfCgkko7.js 7.95 kB │ gzip: 2.57 kB
|
||||
public/build/assets/assisted-BH08iYu-.js 8.06 kB │ gzip: 3.16 kB
|
||||
public/build/assets/value-network-Cp8UYtMs.js 8.06 kB │ gzip: 3.08 kB
|
||||
public/build/assets/editor-d2dUaWdK.js 8.21 kB │ gzip: 2.97 kB
|
||||
public/build/assets/territorial-editorial-B0ni24WA.js 8.22 kB │ gzip: 3.03 kB
|
||||
public/build/assets/opportunity-promotion-CyWpImc1.js 8.40 kB │ gzip: 2.96 kB
|
||||
public/build/assets/publication-history-DfLO6jOc.js 8.42 kB │ gzip: 3.02 kB
|
||||
public/build/assets/print-Tn3aXHlY.js 8.66 kB │ gzip: 3.55 kB
|
||||
public/build/assets/dialog-BY5pP6XP.js 8.81 kB │ gzip: 2.76 kB
|
||||
public/build/assets/shared-DXIXdF9G.js 8.92 kB │ gzip: 3.83 kB
|
||||
public/build/assets/iva-modelo-a-CNylSdBc.js 8.94 kB │ gzip: 3.27 kB
|
||||
public/build/assets/zone-relations-DYVN38G6.js 8.95 kB │ gzip: 2.99 kB
|
||||
public/build/assets/investment-territory-CoaAvOfZ.js 8.96 kB │ gzip: 3.43 kB
|
||||
public/build/assets/similar-businesses-HTwQGVGQ.js 9.11 kB │ gzip: 3.58 kB
|
||||
public/build/assets/interactive-investment-map-C6RYkxx2.js 9.13 kB │ gzip: 3.60 kB
|
||||
public/build/assets/investment-land-preparation-BHeC9B2U.js 9.15 kB │ gzip: 3.33 kB
|
||||
public/build/assets/why-invest-BVa_hDq4.js 9.16 kB │ gzip: 3.49 kB
|
||||
public/build/assets/interest-history-oHNykKmC.js 9.48 kB │ gzip: 3.45 kB
|
||||
public/build/assets/content-hub-CdhSC7BG.js 9.95 kB │ gzip: 2.93 kB
|
||||
public/build/assets/privacy-policy-LI1lpUCK.js 10.36 kB │ gzip: 3.07 kB
|
||||
public/build/assets/editorial-edition-Zq39X7mf.js 10.36 kB │ gzip: 3.47 kB
|
||||
public/build/assets/panel-la1QJ_S6.js 10.46 kB │ gzip: 4.30 kB
|
||||
public/build/assets/result-yez5wOFo.js 10.57 kB │ gzip: 4.73 kB
|
||||
public/build/assets/dashboard-TckpOQI6.js 10.92 kB │ gzip: 3.47 kB
|
||||
public/build/assets/zone-attachments-DS2XtuU6.js 10.94 kB │ gzip: 2.75 kB
|
||||
public/build/assets/business-DjxEhMj7.js 11.10 kB │ gzip: 3.09 kB
|
||||
public/build/assets/detail-Y-uQOHvE.js 11.73 kB │ gzip: 4.31 kB
|
||||
public/build/assets/login-DSqCNzQj.js 11.89 kB │ gzip: 4.92 kB
|
||||
public/build/assets/support-installation-tsBpnQkP.js 12.36 kB │ gzip: 4.86 kB
|
||||
public/build/assets/show-DOVW9TBF.js 12.37 kB │ gzip: 3.99 kB
|
||||
public/build/assets/tempo-brand-BXcnRsyn.js 12.38 kB │ gzip: 5.01 kB
|
||||
public/build/assets/show-cuRBkf3d.js 12.40 kB │ gzip: 4.39 kB
|
||||
public/build/assets/guide-B9b6jazd.js 12.58 kB │ gzip: 4.03 kB
|
||||
public/build/assets/contact-7Fur8Mlo.js 12.65 kB │ gzip: 4.33 kB
|
||||
public/build/assets/project-dossier-document-BLKfe4tL.js 12.68 kB │ gzip: 5.20 kB
|
||||
public/build/assets/dossier-prefill-CkDZdN8F.js 12.72 kB │ gzip: 4.64 kB
|
||||
public/build/assets/technical-incident-GDB_x0wp.js 12.73 kB │ gzip: 4.96 kB
|
||||
public/build/assets/competitiveness-60nqrOfg.js 12.74 kB │ gzip: 4.23 kB
|
||||
public/build/assets/editor-BHONLvty.js 12.91 kB │ gzip: 3.75 kB
|
||||
public/build/assets/fiscal-forms-CElK0P88.js 13.04 kB │ gzip: 4.75 kB
|
||||
public/build/assets/opportunity-classification-Dm_W66ep.js 13.44 kB │ gzip: 4.55 kB
|
||||
public/build/assets/react-CjVep8xp.js 13.90 kB │ gzip: 4.61 kB
|
||||
public/build/assets/opportunity-search-filters-BwOuTqi6.js 14.74 kB │ gzip: 4.72 kB
|
||||
public/build/assets/case-wwTeoOmv.js 14.77 kB │ gzip: 5.38 kB
|
||||
public/build/assets/product-usage-BMGM5Fsp.js 14.81 kB │ gzip: 5.30 kB
|
||||
public/build/assets/company-preparation-Cvt3Dsqm.js 15.17 kB │ gzip: 4.92 kB
|
||||
public/build/assets/terms-of-use-CUQdwKes.js 15.27 kB │ gzip: 4.20 kB
|
||||
public/build/assets/edit-C3UH5lGB.js 15.38 kB │ gzip: 5.15 kB
|
||||
public/build/assets/relationship-repair-brZoqSge.js 16.29 kB │ gzip: 5.62 kB
|
||||
public/build/assets/edit-DpqBs8fP.js 16.60 kB │ gzip: 4.88 kB
|
||||
public/build/assets/edit-CUKYr1QN.js 16.93 kB │ gzip: 4.91 kB
|
||||
public/build/assets/search-DHadj8Kw.js 17.16 kB │ gzip: 5.69 kB
|
||||
public/build/assets/funding-institution-DsJ7OfOI.js 17.24 kB │ gzip: 5.83 kB
|
||||
public/build/assets/use-two-factor-auth-D9w0KtSq.js 17.31 kB │ gzip: 6.30 kB
|
||||
public/build/assets/administration-CgUGyaX7.js 17.41 kB │ gzip: 4.30 kB
|
||||
public/build/assets/notifications-3_Mc-VMV.js 17.61 kB │ gzip: 5.83 kB
|
||||
public/build/assets/ai-BjK1MByI.js 18.08 kB │ gzip: 6.92 kB
|
||||
public/build/assets/project-dossier-report-BiY2DIQg.js 18.98 kB │ gzip: 6.95 kB
|
||||
public/build/assets/submission-B_ilbHzj.js 19.99 kB │ gzip: 6.32 kB
|
||||
public/build/assets/opportunity-resources-CHCOX5-_.js 20.20 kB │ gzip: 7.22 kB
|
||||
public/build/assets/about-BtlawfeW.js 20.95 kB │ gzip: 7.03 kB
|
||||
public/build/assets/zones-OjzXPNvW.js 21.16 kB │ gzip: 5.22 kB
|
||||
public/build/assets/service-editor-DNfxjEmc.js 21.30 kB │ gzip: 6.58 kB
|
||||
public/build/assets/context-DLdtN62b.js 21.56 kB │ gzip: 6.99 kB
|
||||
public/build/assets/declaration-Bzl3lixx.js 22.15 kB │ gzip: 7.14 kB
|
||||
public/build/assets/Combination-BBDI-RbM.js 23.03 kB │ gzip: 8.24 kB
|
||||
public/build/assets/desk-internal-operations-DrgSmbnS.js 23.14 kB │ gzip: 6.54 kB
|
||||
public/build/assets/education-BsVVdx6J.js 23.40 kB │ gzip: 8.19 kB
|
||||
public/build/assets/investment-opportunity-choices-DI6ST39H.js 24.28 kB │ gzip: 8.06 kB
|
||||
public/build/assets/preparation-CykIAPcD.js 24.56 kB │ gzip: 6.73 kB
|
||||
public/build/assets/utils-CCDJ05Ll.js 27.31 kB │ gzip: 8.71 kB
|
||||
public/build/assets/business-wizard-BTqJxN9S.js 28.60 kB │ gzip: 9.42 kB
|
||||
public/build/assets/proactive-investment-DfIQuXvs.js 28.65 kB │ gzip: 9.16 kB
|
||||
public/build/assets/lead-vUXgd2g8.js 29.03 kB │ gzip: 9.96 kB
|
||||
public/build/assets/preparation-ChwKCAm0.js 29.25 kB │ gzip: 8.51 kB
|
||||
public/build/assets/form-G3wES2VJ.js 29.80 kB │ gzip: 8.04 kB
|
||||
public/build/assets/profile-MnSD-8CY.js 30.71 kB │ gzip: 9.33 kB
|
||||
public/build/assets/welcome-DS4fmTKK.js 30.73 kB │ gzip: 6.49 kB
|
||||
public/build/assets/manage-GV_WvYCX.js 31.17 kB │ gzip: 7.63 kB
|
||||
public/build/assets/security-CY6Mu2UR.js 32.11 kB │ gzip: 10.06 kB
|
||||
public/build/assets/editor-CWbYf7YK.js 35.03 kB │ gzip: 11.76 kB
|
||||
public/build/assets/case-Bpi6HtJI.js 37.38 kB │ gzip: 10.33 kB
|
||||
public/build/assets/room-Bg8k9V54.js 38.25 kB │ gzip: 12.14 kB
|
||||
public/build/assets/simulation-form-CVV000gF.js 39.43 kB │ gzip: 10.27 kB
|
||||
public/build/assets/request-B4WvIFJy.js 41.86 kB │ gzip: 11.29 kB
|
||||
public/build/assets/detail-DtYLHmDW.js 46.42 kB │ gzip: 15.18 kB
|
||||
public/build/assets/PortalInvestidor-BE-j5EhK.js 48.72 kB │ gzip: 14.86 kB
|
||||
public/build/assets/business-financial-plan-s1uoiktr.js 49.19 kB │ gzip: 11.98 kB
|
||||
public/build/assets/catalogue-bVIBH9Td.js 50.02 kB │ gzip: 16.10 kB
|
||||
public/build/assets/dropdown-menu-DeBy_kF_.js 57.05 kB │ gzip: 19.02 kB
|
||||
public/build/assets/zone-DkB12lZg.js 62.42 kB │ gzip: 15.63 kB
|
||||
public/build/assets/show-Dzl53Tk_.js 68.49 kB │ gzip: 21.16 kB
|
||||
public/build/assets/opportunity-draft-C9kNlVYV.js 70.65 kB │ gzip: 20.89 kB
|
||||
public/build/assets/document-zip-bqFSTgG4.js 95.97 kB │ gzip: 28.50 kB
|
||||
public/build/assets/app-wUAfzkDv.js 147.52 kB │ gzip: 45.59 kB
|
||||
public/build/assets/leaflet-src-CUWrUTR2.js 148.82 kB │ gzip: 43.39 kB
|
||||
public/build/assets/dist-BRfsww6z.js 307.99 kB │ gzip: 96.67 kB
|
||||
public/build/assets/document-word-CqTGSyXu.js 403.66 kB │ gzip: 115.44 kB
|
||||
public/build/assets/services-C4DSfyGy.js 965.96 kB │ gzip: 226.50 kB
|
||||
public/build/assets/project-dossier-a3IvmE5B.js 1,009.94 kB │ gzip: 274.44 kB
|
||||
|
||||
✓ built in 32.12s
|
||||
[plugin builtin:vite-reporter]
|
||||
(!) Some chunks are larger than 500 kB after minification. Consider:
|
||||
- Using dynamic import() to code-split the application
|
||||
- Use build.rolldownOptions.output.codeSplitting to improve chunking:
|
||||
https://rolldown.rs/reference/OutputOptions.codeSplitting
|
||||
- Adjust chunk size limit for this warning via build.chunkSizeWarningLimit.
|
||||
[33m[PLUGIN_TIMINGS] [0mYour build spent 98% of 32.1s inside plugin hooks (31.4s).
|
||||
Measured inside the callback, so queue time is excluded and time the callback itself awaited is not:
|
||||
- @rolldown/plugin-babel transform (61%, 19.7s, 412 calls)
|
||||
- @laravel/vite-plugin-wayfinder buildStart (20%, 6.6s, 1 call)
|
||||
- @tailwindcss/vite:generate:build transform (14%, 4.4s, 3 calls)
|
||||
- vite:css transform (11%, 3.4s, 3 calls)
|
||||
See https://rolldown.rs/reference/InputOptions.checks#plugintimings for more details.
|
||||
@@ -0,0 +1 @@
|
||||
{"tool":"phpunit","result":"passed","tests":28,"passed":28,"assertions":403,"duration_ms":266357}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<testsuites>
|
||||
<testsuite name="CLI Arguments" tests="28" assertions="403" errors="0" failures="0" skipped="0" time="266.318700">
|
||||
<testsuite name="Tests\Feature\PrivateOfferOperationsTest" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\PrivateOfferOperationsTest.php" tests="13" assertions="180" errors="0" failures="0" skipped="0" time="134.671764">
|
||||
<testcase name="test_withdrawal_is_immediate_with_a_stale_form_and_excludes_every_public_consumer" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\PrivateOfferOperationsTest.php" line="104" class="Tests\Feature\PrivateOfferOperationsTest" classname="Tests.Feature.PrivateOfferOperationsTest" assertions="16" time="17.786787"/>
|
||||
<testcase name="test_reactivation_requires_a_new_exact_validation_and_does_not_resurrect_old_publication" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\PrivateOfferOperationsTest.php" line="122" class="Tests\Feature\PrivateOfferOperationsTest" classname="Tests.Feature.PrivateOfferOperationsTest" assertions="21" time="18.507967"/>
|
||||
<testcase name="test_withdrawal_during_review_can_be_reopened_by_the_owner_without_erasing_history" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\PrivateOfferOperationsTest.php" line="145" class="Tests\Feature\PrivateOfferOperationsTest" classname="Tests.Feature.PrivateOfferOperationsTest" assertions="7" time="7.055661"/>
|
||||
<testcase name="test_expiry_is_immediate_at_the_boundary_frozen_and_independent_of_nda_and_scheduler" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\PrivateOfferOperationsTest.php" line="158" class="Tests\Feature\PrivateOfferOperationsTest" classname="Tests.Feature.PrivateOfferOperationsTest" assertions="16" time="8.647696"/>
|
||||
<testcase name="test_no_deadline_is_invented_when_the_policy_is_pending" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\PrivateOfferOperationsTest.php" line="184" class="Tests\Feature\PrivateOfferOperationsTest" classname="Tests.Feature.PrivateOfferOperationsTest" assertions="7" time="6.434084"/>
|
||||
<testcase name="test_strangers_reviewers_wrong_scopes_and_auditors_cannot_withdraw_or_reactivate" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\PrivateOfferOperationsTest.php" line="195" class="Tests\Feature\PrivateOfferOperationsTest" classname="Tests.Feature.PrivateOfferOperationsTest" assertions="8" time="4.959006"/>
|
||||
<testcase name="test_reports_are_private_idempotent_and_treated_in_the_existing_dashboard_and_tasks" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\PrivateOfferOperationsTest.php" line="207" class="Tests\Feature\PrivateOfferOperationsTest" classname="Tests.Feature.PrivateOfferOperationsTest" assertions="24" time="11.035164"/>
|
||||
<testcase name="test_drafts_cannot_be_reported_by_unrelated_users_and_report_read_requires_current_scope_and_mfa" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\PrivateOfferOperationsTest.php" line="237" class="Tests\Feature\PrivateOfferOperationsTest" classname="Tests.Feature.PrivateOfferOperationsTest" assertions="7" time="4.831239"/>
|
||||
<testcase name="test_highlights_are_versioned_administrative_only_and_cannot_set_commercial_flags" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\PrivateOfferOperationsTest.php" line="250" class="Tests\Feature\PrivateOfferOperationsTest" classname="Tests.Feature.PrivateOfferOperationsTest" assertions="12" time="6.810183"/>
|
||||
<testcase name="test_a_withdrawn_first_draft_still_requires_its_first_editorial_validation" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\PrivateOfferOperationsTest.php" line="268" class="Tests\Feature\PrivateOfferOperationsTest" classname="Tests.Feature.PrivateOfferOperationsTest" assertions="10" time="9.624057"/>
|
||||
<testcase name="test_withdrawal_and_revalidation_do_not_restore_previous_nda_grants" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\PrivateOfferOperationsTest.php" line="280" class="Tests\Feature\PrivateOfferOperationsTest" classname="Tests.Feature.PrivateOfferOperationsTest" assertions="16" time="17.343348"/>
|
||||
<testcase name="test_operational_and_report_screens_use_real_compiled_entries_and_private_cache_headers" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\PrivateOfferOperationsTest.php" line="309" class="Tests\Feature\PrivateOfferOperationsTest" classname="Tests.Feature.PrivateOfferOperationsTest" assertions="23" time="6.368529"/>
|
||||
<testcase name="test_an_edited_private_approval_does_not_extend_the_existing_public_deadline" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\PrivateOfferOperationsTest.php" line="332" class="Tests\Feature\PrivateOfferOperationsTest" classname="Tests.Feature.PrivateOfferOperationsTest" assertions="13" time="15.268044"/>
|
||||
</testsuite>
|
||||
<testsuite name="Tests\Feature\PrivateOfferConfidentialityTest" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\PrivateOfferConfidentialityTest.php" tests="11" assertions="161" errors="0" failures="0" skipped="0" time="106.681963">
|
||||
<testcase name="test_confirmed_specific_fields_save_recover_and_bind_the_editorial_version" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\PrivateOfferConfidentialityTest.php" line="109" class="Tests\Feature\PrivateOfferConfidentialityTest" classname="Tests.Feature.PrivateOfferConfidentialityTest" assertions="9" time="7.920911"/>
|
||||
<testcase name="test_property_fields_are_optional_and_categories_and_unknown_fields_are_enforced" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\PrivateOfferConfidentialityTest.php" line="120" class="Tests\Feature\PrivateOfferConfidentialityTest" classname="Tests.Feature.PrivateOfferConfidentialityTest" assertions="7" time="6.171176"/>
|
||||
<testcase name="test_six_photos_limit_includes_restoration_and_alternate_routes_and_cleans_failed_uploads" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\PrivateOfferConfidentialityTest.php" line="131" class="Tests\Feature\PrivateOfferConfidentialityTest" classname="Tests.Feature.PrivateOfferConfidentialityTest" assertions="15" time="12.991090"/>
|
||||
<testcase name="test_upload_validation_and_quarantine_do_not_grant_publication_or_read_access" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\PrivateOfferConfidentialityTest.php" line="173" class="Tests\Feature\PrivateOfferConfidentialityTest" classname="Tests.Feature.PrivateOfferConfidentialityTest" assertions="9" time="8.173232"/>
|
||||
<testcase name="test_proof_bytes_are_encrypted_and_only_a_scoped_internal_validator_can_read_them" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\PrivateOfferConfidentialityTest.php" line="187" class="Tests\Feature\PrivateOfferConfidentialityTest" classname="Tests.Feature.PrivateOfferConfidentialityTest" assertions="14" time="5.222205"/>
|
||||
<testcase name="test_nda_absence_keeps_access_closed_and_acceptance_alone_never_grants_access" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\PrivateOfferConfidentialityTest.php" line="209" class="Tests\Feature\PrivateOfferConfidentialityTest" classname="Tests.Feature.PrivateOfferConfidentialityTest" assertions="13" time="8.975220"/>
|
||||
<testcase name="test_both_conditions_exact_versions_revocation_and_audit_are_required_on_every_read" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\PrivateOfferConfidentialityTest.php" line="227" class="Tests\Feature\PrivateOfferConfidentialityTest" classname="Tests.Feature.PrivateOfferConfidentialityTest" assertions="18" time="9.934931"/>
|
||||
<testcase name="test_nda_version_changes_withdrawal_session_revocation_and_private_revision_close_access" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\PrivateOfferConfidentialityTest.php" line="259" class="Tests\Feature\PrivateOfferConfidentialityTest" classname="Tests.Feature.PrivateOfferConfidentialityTest" assertions="35" time="19.377575"/>
|
||||
<testcase name="test_nda_integrity_and_authority_loss_are_rechecked_without_exposing_the_company" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\PrivateOfferConfidentialityTest.php" line="299" class="Tests\Feature\PrivateOfferConfidentialityTest" classname="Tests.Feature.PrivateOfferConfidentialityTest" assertions="14" time="10.601570"/>
|
||||
<testcase name="test_compiled_confidential_page_has_no_company_data_before_both_conditions" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\PrivateOfferConfidentialityTest.php" line="323" class="Tests\Feature\PrivateOfferConfidentialityTest" classname="Tests.Feature.PrivateOfferConfidentialityTest" assertions="11" time="6.396441"/>
|
||||
<testcase name="test_photo_metadata_and_original_are_bound_to_the_editorial_fingerprint" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\PrivateOfferConfidentialityTest.php" line="337" class="Tests\Feature\PrivateOfferConfidentialityTest" classname="Tests.Feature.PrivateOfferConfidentialityTest" assertions="16" time="10.917612"/>
|
||||
</testsuite>
|
||||
<testsuite name="Tests\Feature\OfferInvestmentAcceptanceAuditTest" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\OfferInvestmentAcceptanceAuditTest.php" tests="4" assertions="62" errors="0" failures="0" skipped="0" time="24.964973">
|
||||
<testcase name="test_saved_offer_reuses_its_description_in_the_existing_investment_context" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\OfferInvestmentAcceptanceAuditTest.php" line="41" class="Tests\Feature\OfferInvestmentAcceptanceAuditTest" classname="Tests.Feature.OfferInvestmentAcceptanceAuditTest" assertions="8" time="1.306170"/>
|
||||
<testcase name="test_native_context_options_do_not_disclose_another_owners_private_offer" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\OfferInvestmentAcceptanceAuditTest.php" line="55" class="Tests\Feature\OfferInvestmentAcceptanceAuditTest" classname="Tests.Feature.OfferInvestmentAcceptanceAuditTest" assertions="7" time="2.462590"/>
|
||||
<testcase name="test_private_description_is_not_truncated_and_does_not_replace_an_existing_summary" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\OfferInvestmentAcceptanceAuditTest.php" line="67" class="Tests\Feature\OfferInvestmentAcceptanceAuditTest" classname="Tests.Feature.OfferInvestmentAcceptanceAuditTest" assertions="5" time="2.079312"/>
|
||||
<testcase name="test_approved_offer_continues_in_same_dossier_location_analysis_saved_result_and_report_input" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\OfferInvestmentAcceptanceAuditTest.php" line="80" class="Tests\Feature\OfferInvestmentAcceptanceAuditTest" classname="Tests.Feature.OfferInvestmentAcceptanceAuditTest" assertions="42" time="19.116900"/>
|
||||
</testsuite>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
@@ -0,0 +1,3 @@
|
||||
|
||||
> types:check
|
||||
> tsc --noEmit
|
||||
@@ -0,0 +1,3 @@
|
||||
|
||||
> types:check
|
||||
> tsc --noEmit
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"database_tables_rechecked": 219,
|
||||
"tables_changed_since_backup": [],
|
||||
"release_files_rechecked": 417,
|
||||
"release_files_changed": [],
|
||||
"installed_revision": "96f510965f70581a4b41dd94463f598b77dd7cf5",
|
||||
"active_data_writes": 0,
|
||||
"source_read_only_transactions": true
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"remote_main_verified": "3558647e8d82092cd0e31eefbe8b9e5e9eacf573",
|
||||
"guarded_executor_started_for_exact_revision": true,
|
||||
"automatic_path": "paused",
|
||||
"lock_held": true,
|
||||
"other_revisions_forbidden": true
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"executor_status": {
|
||||
"state": "stopped",
|
||||
"target": "3558647e8d82092cd0e31eefbe8b9e5e9eacf573",
|
||||
"previous": "96f510965f70581a4b41dd94463f598b77dd7cf5",
|
||||
"lock_held": false,
|
||||
"error_type": "RuntimeError",
|
||||
"error": "Verification command failed: npm (see protected build log)",
|
||||
"installed_revision": "96f510965f70581a4b41dd94463f598b77dd7cf5",
|
||||
"automatic_path_restored": true
|
||||
},
|
||||
"recovery": {
|
||||
"state": "failed_before_promotion_recovered",
|
||||
"authorised_revision": "3558647e8d82092cd0e31eefbe8b9e5e9eacf573",
|
||||
"installed_revision_preserved": "96f510965f70581a4b41dd94463f598b77dd7cf5",
|
||||
"source_drift": false,
|
||||
"live_promotion": false,
|
||||
"rollback_needed": false,
|
||||
"migration_executed": false,
|
||||
"lock_released": true,
|
||||
"automatic_path_restored": true,
|
||||
"pending_requests": 0,
|
||||
"generated_collision": [
|
||||
{
|
||||
"line": 180,
|
||||
"code": "export const reportForm = (args: { id: string | number } | [id: string | number ] | string | number, options?: RouteQueryOptions): RouteDefinition<'get'> => ({"
|
||||
},
|
||||
{
|
||||
"line": 242,
|
||||
"code": "const reportFormForm = (args: { id: string | number } | [id: string | number ] | string | number, options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({"
|
||||
},
|
||||
{
|
||||
"line": 331,
|
||||
"code": "const reportForm = (args: { id: string | number } | [id: string | number ] | string | number, options?: RouteQueryOptions): RouteFormDefinition<'post'> => ({"
|
||||
}
|
||||
],
|
||||
"failed_release_preserved": [
|
||||
"/var/www/investmoz-homologacao/releases/3558647e8d82092cd0e31eefbe8b9e5e9eacf573-a4916dfe"
|
||||
],
|
||||
"operational_data_writes": 0,
|
||||
"main_not_reverted_or_force_pushed": true
|
||||
},
|
||||
"schema_test_results": [
|
||||
"OK (1 test, 3106 assertions)"
|
||||
],
|
||||
"typescript_errors": [
|
||||
"resources/js/actions/App/Http/Controllers/PrivateOfferOperationsController.ts(180,14): error TS2395: Individual declarations in merged declaration 'reportForm' must be all exported or all local.",
|
||||
"resources/js/actions/App/Http/Controllers/PrivateOfferOperationsController.ts(331,7): error TS2395: Individual declarations in merged declaration 'reportForm' must be all exported or all local."
|
||||
],
|
||||
"npm_build_passed": true,
|
||||
"typed_check_passed": false,
|
||||
"promotion_occurred": false,
|
||||
"installed": "96f510965f70581a4b41dd94463f598b77dd7cf5",
|
||||
"automatic_path": "active",
|
||||
"deploy_service": "inactive",
|
||||
"pending_requests": 0,
|
||||
"served_revision": "96f510965f70581a4b41dd94463f598b77dd7cf5"
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import pathlib,os,json,time,fcntl,importlib.util,uuid,sys
|
||||
TARGET='3558647e8d82092cd0e31eefbe8b9e5e9eacf573';OLD='96f510965f70581a4b41dd94463f598b77dd7cf5'
|
||||
operation=pathlib.Path('/var/lib/investmoz-deploy/offers-exact-3558647e-20261114')
|
||||
spec=importlib.util.spec_from_file_location('investmoz_existing_pipeline','/var/www/deploy/investmoz-pipeline.py');module=importlib.util.module_from_spec(spec);spec.loader.exec_module(module)
|
||||
config=json.loads(pathlib.Path('/etc/investmoz-deploy/delivery.json').read_text());assert config['target_url']=='https://teste-investidor.tempo.co.mz' and config['branch']=='main'
|
||||
runner=module.Delivery(config);state=pathlib.Path(config['state_root'])
|
||||
def status(value,**extra):
|
||||
(operation/'status.json').write_text(json.dumps({'state':value,'target':TARGET,'previous':OLD,'lock_held':True,**extra}))
|
||||
with (state/'delivery.lock').open('a') as lock:
|
||||
fcntl.flock(lock,fcntl.LOCK_EX|fcntl.LOCK_NB)
|
||||
try:
|
||||
assert json.loads((state/'installed.json').read_text())['commit']==OLD
|
||||
assert not list((state/'requests').glob('*.json'))
|
||||
status('reserved_waiting_backup_and_fast_forward',installation_started=False)
|
||||
deadline=time.monotonic()+1200
|
||||
while not (operation/'go.json').exists():
|
||||
if (operation/'abort.json').exists():raise RuntimeError('Authorised operation cancelled before installation')
|
||||
if time.monotonic()>deadline:raise RuntimeError('Reservation timed out without starting installation')
|
||||
time.sleep(1)
|
||||
proof=json.loads((operation/'go.json').read_text());assert proof['target']==TARGET and proof['backup_restore_verified'] is True
|
||||
original_latest=runner.latest
|
||||
def pinned_latest():
|
||||
actual=original_latest()
|
||||
if actual!=TARGET:raise RuntimeError('MAIN_REVISION_MISMATCH: refusing every other revision')
|
||||
return actual
|
||||
runner.latest=pinned_latest
|
||||
assert pinned_latest()==TARGET
|
||||
runner.git('merge-base','--is-ancestor',OLD,TARGET)
|
||||
assert all(x.stem==TARGET for x in (state/'requests').glob('*.json'))
|
||||
with (operation/'build.log').open('wb') as log:
|
||||
runner.log=log;status('installing_exact_revision',installation_started=True)
|
||||
result=runner.deliver(TARGET);runner.log=None
|
||||
assert result.get('commit')==TARGET and result.get('status') in ['installed','already_installed']
|
||||
assert json.loads((state/'installed.json').read_text())['commit']==TARGET
|
||||
(operation/'executor-result.json').write_text(json.dumps(result,indent=2))
|
||||
status('installed_waiting_postverification',installation_started=True,installed_revision=TARGET)
|
||||
deadline=time.monotonic()+1800
|
||||
while not (operation/'finish.json').exists():
|
||||
if (operation/'abort.json').exists():raise RuntimeError('Postverification requires recovery; watcher remains paused')
|
||||
if time.monotonic()>deadline:raise RuntimeError('Postverification timed out; watcher remains paused')
|
||||
time.sleep(1)
|
||||
assert json.loads((operation/'finish.json').read_text())['postverified'] is True
|
||||
assert pinned_latest()==TARGET
|
||||
requests=list((state/'requests').glob('*.json'));assert all(x.stem==TARGET for x in requests)
|
||||
for item in requests:item.rename(state/'processed'/(item.stem+'-exact-offers-'+uuid.uuid4().hex[:8]+'.json'))
|
||||
status('verified_complete',installation_started=True,installed_revision=TARGET)
|
||||
except Exception as error:
|
||||
runner.log=None
|
||||
status('stopped',error_type=type(error).__name__,error=str(error),installed_revision=json.loads((state/'installed.json').read_text()).get('commit'))
|
||||
raise
|
||||
finally:fcntl.flock(lock,fcntl.LOCK_UN)
|
||||
@@ -0,0 +1,95 @@
|
||||
{
|
||||
"authorised_revision": "3558647e8d82092cd0e31eefbe8b9e5e9eacf573",
|
||||
"main_fast_forward_received_exactly": true,
|
||||
"main_after_attempt": "3558647e8d82092cd0e31eefbe8b9e5e9eacf573",
|
||||
"installed_and_served_revision": "96f510965f70581a4b41dd94463f598b77dd7cf5",
|
||||
"installation_result": "FAILED_BEFORE_PROMOTION",
|
||||
"failure": "Wayfinder form variants generated reportForm twice: GET action and POST report helper. TS2395 stopped the existing executor.",
|
||||
"live_source_and_data_preserved": {
|
||||
"tables": 219,
|
||||
"files": 417,
|
||||
"all_unchanged": true
|
||||
},
|
||||
"rollback_needed": false,
|
||||
"automatic_path_restored": true,
|
||||
"pending_requests": 0,
|
||||
"operational_migrations": false,
|
||||
"mne_changed": false,
|
||||
"permissions_changed": false,
|
||||
"public_offers_activated": false,
|
||||
"external_notifications_activated": false,
|
||||
"payments_activated": false,
|
||||
"remediation_scope": "QA only: rename GET controller action to reportPage and adjust route mapping; preserve URLs, auth, data, services and all five phases. No new push or installation.",
|
||||
"corrected_verification": {
|
||||
"production_wayfinder_generation": "passed with form variants, no QA skip",
|
||||
"normal_production_build": "passed",
|
||||
"typescript_after_build": "passed",
|
||||
"phpstan": "full analysis passed, zero errors",
|
||||
"pint": "passed",
|
||||
"phpunit": {
|
||||
"tests": 28,
|
||||
"assertions": 403,
|
||||
"errors": 0,
|
||||
"failures": 0,
|
||||
"suites": [
|
||||
{
|
||||
"name": "Tests\\Feature\\PrivateOfferOperationsTest",
|
||||
"tests": "13",
|
||||
"assertions": "180",
|
||||
"failures": "0",
|
||||
"errors": "0"
|
||||
},
|
||||
{
|
||||
"name": "Tests\\Feature\\PrivateOfferConfidentialityTest",
|
||||
"tests": "11",
|
||||
"assertions": "161",
|
||||
"failures": "0",
|
||||
"errors": "0"
|
||||
},
|
||||
{
|
||||
"name": "Tests\\Feature\\OfferInvestmentAcceptanceAuditTest",
|
||||
"tests": "4",
|
||||
"assertions": "62",
|
||||
"failures": "0",
|
||||
"errors": "0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"test_database": "offers_wayfinder_fix_20261114_test, loopback:55441",
|
||||
"user_preview_database_and_accepted_evidence_preserved": true
|
||||
},
|
||||
"requires_new_exact_revision_authorisation": true,
|
||||
"evidence_hash_encoding": "SHA256 of UTF-8 text with Git LF normalisation",
|
||||
"evidence_sha256": {
|
||||
"backup-summary.json": "a67f9bfb9229b8622607bf0e43deb76ee3d2161209a465da5dcb89b233e363a3",
|
||||
"corrected-integrated-report-input.json": "e41bc34459b1ff94cbf5050c36e56634bae0c0ab591f0778b3ed0646824a7a85",
|
||||
"corrected-phpstan.log": "c10f74fb840de989bba7e9c2dfee7f8419b5d2ac0cc0daf18a4a187f6530aa43",
|
||||
"corrected-production-build.log": "7e2dfa067638e1c9adf6fbdd40e04ba83caa5f9fd922b48d2a2f0cb02bf6a987",
|
||||
"corrected-regression.log": "eb3f9b8535270480ec764a46fb4c2942027128a28cd08b898be2f5e61ef4e4a9",
|
||||
"corrected-regression.xml": "d3a8675085940d4dffebd46c91559dcdfaf8fdf74801e8af8502e1c332676599",
|
||||
"corrected-types-after-build.log": "b59cdb3ff539a29c8ae6c37772ecc1a93a5266c49959d408f16b17487b7ac755",
|
||||
"corrected-types.log": "b59cdb3ff539a29c8ae6c37772ecc1a93a5266c49959d408f16b17487b7ac755",
|
||||
"data-integrity.json": "d3bdd17f1c28f7e54104a65fde84b58615a1a0cdd43651a9e9bb99bd62f956ad",
|
||||
"exact-sha-start.json": "726f44fe33dadeebe378bd0fecd70af3d63d8705151bec7708b35b6600c3d38b",
|
||||
"executor-failure.json": "7fbf9a0e605f347f1e7b3fa2abd68e38776ef2b9296528331bff03cb63fab168",
|
||||
"guarded-executor-used.py": "b9a34edefd4de3f6a6077fa06b3eb28641b3ab4d79e60ead025299d02ddc4d00",
|
||||
"recovery.json": "e7c0ecab88759b40bda6d5856cfbbb856f598ac6bc808ff38524e9c5eb35a2d4",
|
||||
"reservation-context.json": "b83e5c1ba9ebaa5a6568e49c05e3707b642b8c248d8b47c71ece7a5a80b8bc02",
|
||||
"reservation.json": "5d544d573d12f470d6ff8eec16b8ba6582dd151ef6bf1c49206ae2d935691346",
|
||||
"restore-summary.json": "c7ac5cc3bee9a456127b27aff87193739b2d1380e55eb8f14355a1a66db078ba",
|
||||
"safeguard-verified.json": "3e8b37dc60ff110a8731834bcfa680e584a330015912692312f012bac61f8689",
|
||||
"wayfinder-generation.log": "0fe46f117980a58f4e45f763d67dcfc5e1bc221809ec089a3f787c856fca9267"
|
||||
},
|
||||
"corrective_source_sha256": {
|
||||
"app/Http/Controllers/PrivateOfferOperationsController.php": "15da904c466be9bb38fa9a673beff55816026caf72569bc4289d399c77f10cce",
|
||||
"routes/web.php": "50dbec32d17dbbd334298eb0bf77585b15d98781c9368fcb3f398642b83b2168"
|
||||
},
|
||||
"functional_delta_since_authorised_revision": [
|
||||
"app/Http/Controllers/PrivateOfferOperationsController.php",
|
||||
"routes/web.php"
|
||||
],
|
||||
"generated_symbols_unique": {
|
||||
"reportForm": 1,
|
||||
"reportPage": 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"state": "failed_before_promotion_recovered",
|
||||
"authorised_revision": "3558647e8d82092cd0e31eefbe8b9e5e9eacf573",
|
||||
"installed_revision_preserved": "96f510965f70581a4b41dd94463f598b77dd7cf5",
|
||||
"source_drift": false,
|
||||
"live_promotion": false,
|
||||
"rollback_needed": false,
|
||||
"migration_executed": false,
|
||||
"lock_released": true,
|
||||
"automatic_path_restored": true,
|
||||
"pending_requests": 0,
|
||||
"generated_collision": [
|
||||
{
|
||||
"line": 180,
|
||||
"code": "export const reportForm = (args: { id: string | number } | [id: string | number ] | string | number, options?: RouteQueryOptions): RouteDefinition<'get'> => ({"
|
||||
},
|
||||
{
|
||||
"line": 242,
|
||||
"code": "const reportFormForm = (args: { id: string | number } | [id: string | number ] | string | number, options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({"
|
||||
},
|
||||
{
|
||||
"line": 331,
|
||||
"code": "const reportForm = (args: { id: string | number } | [id: string | number ] | string | number, options?: RouteQueryOptions): RouteFormDefinition<'post'> => ({"
|
||||
}
|
||||
],
|
||||
"failed_release_preserved": [
|
||||
"/var/www/investmoz-homologacao/releases/3558647e8d82092cd0e31eefbe8b9e5e9eacf573-a4916dfe"
|
||||
],
|
||||
"operational_data_writes": 0,
|
||||
"main_not_reverted_or_force_pushed": true
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"temporary_reservation_relaunched_before_build": true,
|
||||
"executor_umask_preserved": "0027",
|
||||
"authorised_webhook_requests_reserved": 1,
|
||||
"served_revision": "96f51096",
|
||||
"installation_started": false
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"watcher": "paused",
|
||||
"served_revision_preserved": "96f51096",
|
||||
"reservation_unit": "investmoz-offers-exact-3558647e-20261114",
|
||||
"automatic_latest_loop_not_used": true,
|
||||
"existing_executor_delegated_without_modification": true
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"restore_verified": true,
|
||||
"all_table_counts_and_checksums_match": true,
|
||||
"tables": 219,
|
||||
"isolated_network": "none",
|
||||
"isolated_database": "offers_install_restore_20261114_test",
|
||||
"isolated_port": 55441,
|
||||
"schema_changes": 0,
|
||||
"operational_data_writes": 0,
|
||||
"files_restored_and_verified": 417,
|
||||
"encryption": {
|
||||
"encrypted_originals_recovered": 0,
|
||||
"mfa_envelopes_recovered": 2,
|
||||
"preserved_keys_verified": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"backup_restore_verified": true,
|
||||
"tables": 219,
|
||||
"files": 417,
|
||||
"dump_sha256": "88152dc39367f5e59eb072f63b44d3a949e93d73ae855eccb33c3c0b9e37bf6c",
|
||||
"files_sha256": "aa6248e0eec3d27d58a4bbb3c16848a70dcb3524e6cafc6340b682984a8bbdb3",
|
||||
"release_drift": false,
|
||||
"exclusive_lock": true,
|
||||
"automatic_path": "paused",
|
||||
"pending_requests": 0,
|
||||
"installation_started": false
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
|
||||
[32m [Wayfinder] Generated actions in C:\laragon\www\portal-investidor-qa-editorial-20261113\resources\js\actions[39m
|
||||
|
||||
[32m [Wayfinder] Generated routes in C:\laragon\www\portal-investidor-qa-editorial-20261113\resources\js\routes[39m
|
||||
@@ -0,0 +1 @@
|
||||
{"tool":"phpunit","result":"failed","tests":2,"passed":1,"assertions":11,"duration_ms":23058,"failed":1,"failures":[{"test":"Tests\\Feature\\OfferInvestmentAcceptanceAuditTest::test_saved_offer_reuses_its_description_in_the_existing_investment_context","file":"C:\\laragon\\www\\portal-investidor-qa-editorial-20261113\\tests\\Feature\\OfferInvestmentAcceptanceAuditTest.php","line":43,"message":"Write once/read always: the existing description must not disappear at the investment-context boundary.\nFailed asserting that null is identical to 'Existing company description to reuse once in investment preparation.'."}]}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<testsuites>
|
||||
<testsuite name="Tests\Feature\OfferInvestmentAcceptanceAuditTest" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\OfferInvestmentAcceptanceAuditTest.php" tests="2" assertions="11" errors="0" failures="1" skipped="0" time="23.049038">
|
||||
<testcase name="test_saved_offer_reuses_its_description_in_the_existing_investment_context" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\OfferInvestmentAcceptanceAuditTest.php" line="37" class="Tests\Feature\OfferInvestmentAcceptanceAuditTest" classname="Tests.Feature.OfferInvestmentAcceptanceAuditTest" assertions="4" time="17.378447">
|
||||
<failure type="PHPUnit\Framework\ExpectationFailedException">Tests\Feature\OfferInvestmentAcceptanceAuditTest::test_saved_offer_reuses_its_description_in_the_existing_investment_context
|
||||
Write once/read always: the existing description must not disappear at the investment-context boundary.
|
||||
Failed asserting that null is identical to 'Existing company description to reuse once in investment preparation.'.
|
||||
|
||||
C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\OfferInvestmentAcceptanceAuditTest.php:43</failure>
|
||||
</testcase>
|
||||
<testcase name="test_native_context_options_do_not_disclose_another_owners_private_offer" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\OfferInvestmentAcceptanceAuditTest.php" line="47" class="Tests\Feature\OfferInvestmentAcceptanceAuditTest" classname="Tests.Feature.OfferInvestmentAcceptanceAuditTest" assertions="7" time="5.670591"/>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"format_failures": 61,
|
||||
"baseline_paths": 61,
|
||||
"intersection_with_module_delta": [],
|
||||
"all_git_files_unchanged_since_phase1_closure": true,
|
||||
"only_matrix_row_changed": "RF-MN-003"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"tool":"phpunit","result":"passed","tests":14,"passed":14,"assertions":227,"duration_ms":22125}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<testsuites>
|
||||
<testsuite name="CLI Arguments" tests="14" assertions="227" errors="0" failures="0" skipped="0" time="22.109963">
|
||||
<testsuite name="Tests\Feature\OpportunityManagementTest" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\OpportunityManagementTest.php" tests="3" assertions="46" errors="0" failures="0" skipped="0" time="11.864628">
|
||||
<testcase name="test_installation_requires_management_permission_and_current_proofs" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\OpportunityManagementTest.php" line="20" class="Tests\Feature\OpportunityManagementTest" classname="Tests.Feature.OpportunityManagementTest" assertions="6" time="9.826727"/>
|
||||
<testcase name="test_incomplete_draft_recovers_and_soft_delete_restore_preserves_identity" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\OpportunityManagementTest.php" line="32" class="Tests\Feature\OpportunityManagementTest" classname="Tests.Feature.OpportunityManagementTest" assertions="21" time="1.154303"/>
|
||||
<testcase name="test_closed_version_edits_do_not_change_public_values_classification_or_relations" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\OpportunityManagementTest.php" line="60" class="Tests\Feature\OpportunityManagementTest" classname="Tests.Feature.OpportunityManagementTest" assertions="19" time="0.883598"/>
|
||||
</testsuite>
|
||||
<testsuite name="Tests\Feature\OpportunitySearchTest" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\OpportunitySearchTest.php" tests="6" assertions="112" errors="0" failures="0" skipped="0" time="6.293386">
|
||||
<testcase name="test_one_park_selector_matches_public_associations_not_proximity_or_shared_territory" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\OpportunitySearchTest.php" line="26" class="Tests\Feature\OpportunitySearchTest" classname="Tests.Feature.OpportunitySearchTest" assertions="9" time="1.627457"/>
|
||||
<testcase name="test_historical_and_current_preparation_with_equal_numbers_are_not_equivalent" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\OpportunitySearchTest.php" line="38" class="Tests\Feature\OpportunitySearchTest" classname="Tests.Feature.OpportunitySearchTest" assertions="6" time="0.509868"/>
|
||||
<testcase name="test_amount_requires_currency_and_combines_with_sector_location_and_modality" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\OpportunitySearchTest.php" line="47" class="Tests\Feature\OpportunitySearchTest" classname="Tests.Feature.OpportunitySearchTest" assertions="10" time="0.710202"/>
|
||||
<testcase name="test_zone_options_are_not_filtered_by_opportunity_text_or_modality" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\OpportunitySearchTest.php" line="58" class="Tests\Feature\OpportunitySearchTest" classname="Tests.Feature.OpportunitySearchTest" assertions="7" time="0.531233"/>
|
||||
<testcase name="test_list_map_and_export_keep_the_same_public_park_results" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\OpportunitySearchTest.php" line="67" class="Tests\Feature\OpportunitySearchTest" classname="Tests.Feature.OpportunitySearchTest" assertions="16" time="1.179871"/>
|
||||
<testcase name="test_public_catalogue_consumers_keep_their_own_result_collections" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\OpportunitySearchTest.php" line="80" class="Tests\Feature\OpportunitySearchTest" classname="Tests.Feature.OpportunitySearchTest" assertions="64" time="1.734754"/>
|
||||
</testsuite>
|
||||
<testsuite name="Tests\Feature\OpportunityResourcesTest" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\OpportunityResourcesTest.php" tests="5" assertions="69" errors="0" failures="0" skipped="0" time="3.951950">
|
||||
<testcase name="test_encrypted_original_scan_pending_recovery_replacement_and_logical_lifecycle" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\OpportunityResourcesTest.php" line="47" class="Tests\Feature\OpportunityResourcesTest" classname="Tests.Feature.OpportunityResourcesTest" assertions="24" time="0.989391"/>
|
||||
<testcase name="test_multiple_locations_without_coordinates_and_invalid_hierarchy_and_geometry" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\OpportunityResourcesTest.php" line="82" class="Tests\Feature\OpportunityResourcesTest" classname="Tests.Feature.OpportunityResourcesTest" assertions="14" time="0.667104"/>
|
||||
<testcase name="test_foreign_country_rejects_mozambican_hierarchy_without_changing_the_saved_draft" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\OpportunityResourcesTest.php" line="100" class="Tests\Feature\OpportunityResourcesTest" classname="Tests.Feature.OpportunityResourcesTest" assertions="20" time="1.242190"/>
|
||||
<testcase name="test_invalid_upload_and_cross_opportunity_links_do_not_create_resources" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\OpportunityResourcesTest.php" line="132" class="Tests\Feature\OpportunityResourcesTest" classname="Tests.Feature.OpportunityResourcesTest" assertions="7" time="0.641653"/>
|
||||
<testcase name="test_image_processing_is_explicit_and_every_derivative_requires_its_own_scan" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\OpportunityResourcesTest.php" line="141" class="Tests\Feature\OpportunityResourcesTest" classname="Tests.Feature.OpportunityResourcesTest" assertions="4" time="0.411613"/>
|
||||
</testsuite>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
@@ -0,0 +1,187 @@
|
||||
{
|
||||
"candidate": "528a2597",
|
||||
"phase_ancestry": {
|
||||
"1f2990e4": true,
|
||||
"6e302db6": true,
|
||||
"91c18573": true,
|
||||
"7691cab3": true,
|
||||
"e431de8b": true,
|
||||
"528a2597": true
|
||||
},
|
||||
"hash_checks": [
|
||||
{
|
||||
"path": "app\\Console\\Commands\\ExpirePrivateOffers.php",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"path": "app\\Http\\Controllers\\OfferPublicCatalogueController.php",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"path": "app\\Http\\Controllers\\PortalEditorialWorkflowController.php",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"path": "app\\Http\\Controllers\\PrivateOfferOperationsController.php",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"path": "app\\Http\\Controllers\\PrivateOfferPreparationController.php",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"path": "app\\Services\\OfferAvailability.php",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"path": "app\\Services\\OfferPublicCatalogue.php",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"path": "app\\Services\\OpportunityProfileWorkflow.php",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"path": "app\\Services\\PrivateOfferConfidentiality.php",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"path": "app\\Services\\PrivateOfferOperations.php",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"path": "app\\Services\\PrivateOfferPreparation.php",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"path": "config\\private_offers.php",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"path": "docs\\investmoz\\unification\\traceability-and-manual.md",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"path": "docs\\investmoz\\v2\\auditoria-portal\\matriz-requisitos.csv",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"path": "docs\\investmoz\\v2\\auditoria-portal\\ofertas-fase5-fecho-qa-20261114.md",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"path": "resources\\js\\components\\offer-operations.tsx",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"path": "resources\\js\\components\\public-offer-card.tsx",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"path": "resources\\js\\lib\\public-offer.ts",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"path": "resources\\js\\pages\\management\\editorial-workflow.tsx",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"path": "resources\\js\\pages\\offers\\preparation-index.tsx",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"path": "resources\\js\\pages\\offers\\preparation.tsx",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"path": "resources\\js\\pages\\offers\\public-detail.tsx",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"path": "resources\\js\\pages\\offers\\report.tsx",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"path": "routes\\console.php",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"path": "routes\\web.php",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"path": "tests\\Feature\\PrivateOfferOperationsTest.php",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"path": "outputs\\offers-private-phase5-20261114\\focused.log",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"path": "outputs\\offers-private-phase5-20261114\\focused.xml",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"path": "outputs\\offers-private-phase5-20261114\\regression.log",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"path": "outputs\\offers-private-phase5-20261114\\regression.xml",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"path": "outputs\\offers-private-phase5-20261114\\global-format.log",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"path": "outputs\\offers-private-phase5-20261114\\build-final.log",
|
||||
"matches": true
|
||||
}
|
||||
],
|
||||
"all_hashes_match": true,
|
||||
"junit_suites": [
|
||||
{
|
||||
"name": "Tests\\Feature\\PrivateOfferOperationsTest",
|
||||
"tests": "13",
|
||||
"assertions": "180",
|
||||
"failures": "0",
|
||||
"errors": "0"
|
||||
},
|
||||
{
|
||||
"name": "Tests\\Feature\\PrivateOfferConfidentialityTest",
|
||||
"tests": "11",
|
||||
"assertions": "160",
|
||||
"failures": "0",
|
||||
"errors": "0"
|
||||
},
|
||||
{
|
||||
"name": "Tests\\Feature\\OfferPublicCatalogueTest",
|
||||
"tests": "10",
|
||||
"assertions": "133",
|
||||
"failures": "0",
|
||||
"errors": "0"
|
||||
},
|
||||
{
|
||||
"name": "Tests\\Feature\\PrivateOfferPreparationTest",
|
||||
"tests": "14",
|
||||
"assertions": "146",
|
||||
"failures": "0",
|
||||
"errors": "0"
|
||||
},
|
||||
{
|
||||
"name": "Tests\\Feature\\PrivateOfferEditorialTest",
|
||||
"tests": "11",
|
||||
"assertions": "161",
|
||||
"failures": "0",
|
||||
"errors": "0"
|
||||
}
|
||||
],
|
||||
"totals": {
|
||||
"tests": 59,
|
||||
"assertions": 780,
|
||||
"failures": 0,
|
||||
"errors": 0
|
||||
},
|
||||
"migration_delta": [],
|
||||
"execution": "Read-only verification of accepted receipts; no operational connection or installation."
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"candidate": "9a01426864a47dacd97db2b4da363aba86a94df6",
|
||||
"bundle": "offers-9a014268.bundle",
|
||||
"bundle_sha256": "446309507638faed558922a4f1a1c515ae4cf2e47ad4e78f4e125a4f61b2ef35",
|
||||
"required_git_commit": "96f510965f70581a4b41dd94463f598b77dd7cf5",
|
||||
"bundle_verification": "The bundle contains this ref:\n9a01426864a47dacd97db2b4da363aba86a94df6 refs/heads/qa/editorial-fase1-20261113\nThe bundle requires this ref:\n96f510965f70581a4b41dd94463f598b77dd7cf5 \nThe bundle uses this hash algorithm: sha1\noutputs/offers-homologation-preparation-20261114/offers-9a014268.bundle is okay\n",
|
||||
"report": "docs/investmoz/v2/auditoria-portal/ofertas-prontidao-homologacao-20261114.md",
|
||||
"target": "https://teste-investidor.tempo.co.mz",
|
||||
"target_current_revision_verified": false,
|
||||
"installation_authorized": false,
|
||||
"public_activation_authorized": false,
|
||||
"matrix_other_rows_preserved": true,
|
||||
"acceptance": "Private QA integration accepted; controlled private homologation recommended subject to destination preflight, current backup/restore, compatible rollback and explicit authorisation."
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
{
|
||||
"candidate": "9a01426864a47dacd97db2b4da363aba86a94df6",
|
||||
"historical_destination_receipt": "outputs/administration-dashboard-hierarchy-20261114/destination-installation-verified.json",
|
||||
"historical_destination_revision": "96f510965f70581a4b41dd94463f598b77dd7cf5",
|
||||
"destination_live_verified": false,
|
||||
"phase_ancestry": {
|
||||
"1f2990e4": true,
|
||||
"6e302db6": true,
|
||||
"91c18573": true,
|
||||
"7691cab3": true,
|
||||
"e431de8b": true,
|
||||
"528a2597": true,
|
||||
"ed2fd86f": true,
|
||||
"9a014268": true
|
||||
},
|
||||
"receipt_hash_checks": [
|
||||
{
|
||||
"commit": "528a2597",
|
||||
"path": "app/Console/Commands/ExpirePrivateOffers.php",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"commit": "528a2597",
|
||||
"path": "app/Http/Controllers/OfferPublicCatalogueController.php",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"commit": "528a2597",
|
||||
"path": "app/Http/Controllers/PortalEditorialWorkflowController.php",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"commit": "528a2597",
|
||||
"path": "app/Http/Controllers/PrivateOfferOperationsController.php",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"commit": "528a2597",
|
||||
"path": "app/Http/Controllers/PrivateOfferPreparationController.php",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"commit": "528a2597",
|
||||
"path": "app/Services/OfferAvailability.php",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"commit": "528a2597",
|
||||
"path": "app/Services/OfferPublicCatalogue.php",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"commit": "528a2597",
|
||||
"path": "app/Services/OpportunityProfileWorkflow.php",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"commit": "528a2597",
|
||||
"path": "app/Services/PrivateOfferConfidentiality.php",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"commit": "528a2597",
|
||||
"path": "app/Services/PrivateOfferOperations.php",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"commit": "528a2597",
|
||||
"path": "app/Services/PrivateOfferPreparation.php",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"commit": "528a2597",
|
||||
"path": "config/private_offers.php",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"commit": "528a2597",
|
||||
"path": "docs/investmoz/unification/traceability-and-manual.md",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"commit": "528a2597",
|
||||
"path": "docs/investmoz/v2/auditoria-portal/matriz-requisitos.csv",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"commit": "528a2597",
|
||||
"path": "docs/investmoz/v2/auditoria-portal/ofertas-fase5-fecho-qa-20261114.md",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"commit": "528a2597",
|
||||
"path": "resources/js/components/offer-operations.tsx",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"commit": "528a2597",
|
||||
"path": "resources/js/components/public-offer-card.tsx",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"commit": "528a2597",
|
||||
"path": "resources/js/lib/public-offer.ts",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"commit": "528a2597",
|
||||
"path": "resources/js/pages/management/editorial-workflow.tsx",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"commit": "528a2597",
|
||||
"path": "resources/js/pages/offers/preparation-index.tsx",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"commit": "528a2597",
|
||||
"path": "resources/js/pages/offers/preparation.tsx",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"commit": "528a2597",
|
||||
"path": "resources/js/pages/offers/public-detail.tsx",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"commit": "528a2597",
|
||||
"path": "resources/js/pages/offers/report.tsx",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"commit": "528a2597",
|
||||
"path": "routes/console.php",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"commit": "528a2597",
|
||||
"path": "routes/web.php",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"commit": "528a2597",
|
||||
"path": "tests/Feature/PrivateOfferOperationsTest.php",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"commit": "528a2597",
|
||||
"path": "outputs/offers-private-phase5-20261114/focused.log",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"commit": "528a2597",
|
||||
"path": "outputs/offers-private-phase5-20261114/focused.xml",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"commit": "528a2597",
|
||||
"path": "outputs/offers-private-phase5-20261114/regression.log",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"commit": "528a2597",
|
||||
"path": "outputs/offers-private-phase5-20261114/regression.xml",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"commit": "528a2597",
|
||||
"path": "outputs/offers-private-phase5-20261114/global-format.log",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"commit": "528a2597",
|
||||
"path": "outputs/offers-private-phase5-20261114/build-final.log",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"commit": "9a014268",
|
||||
"path": "app/Services/InvestorContext.php",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"commit": "9a014268",
|
||||
"path": "tests/Feature/OfferInvestmentAcceptanceAuditTest.php",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"commit": "9a014268",
|
||||
"path": "tests/Feature/PrivateOfferConfidentialityTest.php",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"commit": "9a014268",
|
||||
"path": "tests/frontend/offer-investment-continuity.test.mjs",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"commit": "9a014268",
|
||||
"path": "outputs/offers-investment-continuity-20261114/focused.log",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"commit": "9a014268",
|
||||
"path": "outputs/offers-investment-continuity-20261114/focused.xml",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"commit": "9a014268",
|
||||
"path": "outputs/offers-investment-continuity-20261114/frontend-regression.log",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"commit": "9a014268",
|
||||
"path": "outputs/offers-investment-continuity-20261114/integrated-report-input.json",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"commit": "9a014268",
|
||||
"path": "outputs/offers-investment-continuity-20261114/location-analysis.json",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"commit": "9a014268",
|
||||
"path": "outputs/offers-investment-continuity-20261114/phpstan.log",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"commit": "9a014268",
|
||||
"path": "outputs/offers-investment-continuity-20261114/private-integrated-report.docx",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"commit": "9a014268",
|
||||
"path": "outputs/offers-investment-continuity-20261114/private-integrated-report.html",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"commit": "9a014268",
|
||||
"path": "outputs/offers-investment-continuity-20261114/regression.log",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"commit": "9a014268",
|
||||
"path": "outputs/offers-investment-continuity-20261114/regression.xml",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"commit": "9a014268",
|
||||
"path": "outputs/offers-investment-continuity-20261114/report-test.log",
|
||||
"matches": true
|
||||
},
|
||||
{
|
||||
"commit": "9a014268",
|
||||
"path": "outputs/offers-investment-continuity-20261114/types.log",
|
||||
"matches": true
|
||||
}
|
||||
],
|
||||
"all_hashes_match": true,
|
||||
"migration_or_lockfile_delta_since_historical_destination": [],
|
||||
"local_php": {
|
||||
"version": "8.5.11",
|
||||
"gd": false,
|
||||
"imagewebp": false
|
||||
},
|
||||
"publication": "closed by model/workflow/native scope and normal OfferPublicCatalogue; synthetic injection only in isolated testing",
|
||||
"defaults": {
|
||||
"nda": null,
|
||||
"validity_days": null,
|
||||
"validity_policy_reference": null,
|
||||
"expiry_scheduler_enabled": false
|
||||
},
|
||||
"installation_authorized": false,
|
||||
"installation_executed": false,
|
||||
"backup_current_destination_created": false,
|
||||
"restore_current_destination_tested": false,
|
||||
"real_email_sent": false,
|
||||
"product_files_modified_this_preparation": false
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"tool":"phpunit","result":"passed","tests":4,"passed":4,"assertions":55,"duration_ms":25651}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<testsuites>
|
||||
<testsuite name="Tests\Feature\OfferInvestmentAcceptanceAuditTest" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\OfferInvestmentAcceptanceAuditTest.php" tests="4" assertions="55" errors="0" failures="0" skipped="0" time="25.647841">
|
||||
<testcase name="test_saved_offer_reuses_its_description_in_the_existing_investment_context" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\OfferInvestmentAcceptanceAuditTest.php" line="41" class="Tests\Feature\OfferInvestmentAcceptanceAuditTest" classname="Tests.Feature.OfferInvestmentAcceptanceAuditTest" assertions="8" time="6.981317"/>
|
||||
<testcase name="test_native_context_options_do_not_disclose_another_owners_private_offer" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\OfferInvestmentAcceptanceAuditTest.php" line="55" class="Tests\Feature\OfferInvestmentAcceptanceAuditTest" classname="Tests.Feature.OfferInvestmentAcceptanceAuditTest" assertions="7" time="2.323249"/>
|
||||
<testcase name="test_private_description_is_not_truncated_and_does_not_replace_an_existing_summary" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\OfferInvestmentAcceptanceAuditTest.php" line="67" class="Tests\Feature\OfferInvestmentAcceptanceAuditTest" classname="Tests.Feature.OfferInvestmentAcceptanceAuditTest" assertions="5" time="2.097139"/>
|
||||
<testcase name="test_approved_offer_continues_in_same_dossier_location_analysis_saved_result_and_report_input" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\OfferInvestmentAcceptanceAuditTest.php" line="80" class="Tests\Feature\OfferInvestmentAcceptanceAuditTest" classname="Tests.Feature.OfferInvestmentAcceptanceAuditTest" assertions="35" time="14.246136"/>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
@@ -0,0 +1,49 @@
|
||||
✔ financial forecast reconciles revenue, finance sources, working capital and monthly cash (25.013875ms)
|
||||
✔ opening payable installments reduce cash without changing profit and retain unpaid balances (4.962292ms)
|
||||
✔ opening payable schedule caps cent rounding at the remaining balance (0.817417ms)
|
||||
✔ monthly unit schedules drive revenue, costs, inventory and scenario volume (11.337458ms)
|
||||
✔ monthly unit schedules must match the horizon and contain valid quantities (2.604459ms)
|
||||
✔ monthly prices and unit costs affect revenue, consumption and scenarios (4.282666ms)
|
||||
✔ monthly money schedules cover the horizon and reject invalid amounts (2.09225ms)
|
||||
✔ monthly operating and payroll expense schedules remain separate from payments (3.289125ms)
|
||||
✔ monthly operating schedules require full horizon, exact cents and current snapshot version (18.074292ms)
|
||||
✔ losses and negative closing cash remain visible instead of being clamped to zero (3.032334ms)
|
||||
✔ principal repayments affect cash but are excluded from profit expenses (2.658625ms)
|
||||
✔ depreciation allocation reconciles exactly over useful life (1.653209ms)
|
||||
✔ missing currency, products, invalid numbers and invalid horizons are explicit errors (1.136708ms)
|
||||
✔ versioned plan snapshots restore assumptions and reject unsupported or oversized data (3.091333ms)
|
||||
✔ business plan serializer refuses snapshots above the server storage limit (0.349541ms)
|
||||
✔ sales invoicing and receipts follow an optional declared collection lag (0.986292ms)
|
||||
✔ scheduled collection percentage keeps unpaid invoices in receivables with exact rounding (0.648709ms)
|
||||
✔ sales recovery applies only to overdue invoices and never exceeds open balances (1.041541ms)
|
||||
✔ opening receivables affect cash only when a month and collection percentage are declared (0.967791ms)
|
||||
✔ opening receivable installments reconcile to the initial balance across months (1.622958ms)
|
||||
✔ opening receivable installments cap independently rounded cents at the available balance (2.369541ms)
|
||||
✔ inventory purchases, consumption, closing stock and stock shortfall reconcile independently of payments (0.385875ms)
|
||||
✔ inventory lead time separates monthly orders, receipts, shortages and goods still in transit (0.667ms)
|
||||
✔ monthly inventory purchase schedule reconciles variable orders, lead times, receipts and transit (1.319042ms)
|
||||
✔ monthly variable purchase payments affect cash independently from inventory orders and profit (0.722625ms)
|
||||
✔ product inventory schedules reconcile independently and do not transfer stock between products (6.993292ms)
|
||||
✔ supplier payment terms drive purchase cash flow and expose amounts due beyond the horizon (2.183292ms)
|
||||
✔ supplier order installments reconcile exact cents, cash dates and unallocated balances (0.841625ms)
|
||||
✔ business assumptions survive the existing encrypted-draft snapshot split and restore (0.779958ms)
|
||||
✔ presentation currency conversion requires and reports its rate, date, and declared source (1.636458ms)
|
||||
✔ CSV marks presentation currency conversion as an unverified user planning assumption (1.087958ms)
|
||||
✔ business plan export carries its assumptions, formulas, period, currency, and matching result (0.447959ms)
|
||||
✔ business plan CSV preserves declared monthly product schedules (0.446125ms)
|
||||
✔ CSV export exposes the sales collection assumption and outstanding receivable (0.320583ms)
|
||||
✔ business plan CSV neutralizes formula-like labels and preserves negative financial values (0.323792ms)
|
||||
✔ financial scenarios expose user-declared changes without creating tax values (5.431667ms)
|
||||
✔ one dossier groups stage evidence, SWOT, fiscal and all preserved APIEX versions without modifying sources (5.195167ms)
|
||||
✔ cross-dossier or cross-opportunity preparation is rejected and an ambiguous scope requires selection (1.404625ms)
|
||||
✔ financial results from a different opportunity do not enter the consolidated report or its index (0.736833ms)
|
||||
✔ the consolidated report reuses loan, break-even, financing and budget outputs with currency and historical state (14.746791ms)
|
||||
✔ the existing Word and print exporters retain dossier identity, pending stages, source and history, escaping active HTML (131.861583ms)
|
||||
ℹ tests 41
|
||||
ℹ suites 0
|
||||
ℹ pass 41
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 964.394334
|
||||
@@ -0,0 +1,663 @@
|
||||
{
|
||||
"path": {
|
||||
"funding_selections": [],
|
||||
"preparation_action": null,
|
||||
"private_investment": null,
|
||||
"case": {
|
||||
"id": "f71a2833-c182-43ba-862d-234e4b64739b",
|
||||
"title": "Synthetic acquisition acceptance",
|
||||
"version": 5,
|
||||
"closed": false
|
||||
},
|
||||
"journey_id": "a0851462-2b7f-47dc-bb0f-59c2a5004e36",
|
||||
"definition_id": "61000919-0000-4000-8000-000000000001",
|
||||
"definition_version": 1,
|
||||
"source_sha256": "aa13c53138ec15cd808de43d9dc2fc6808da86943d09d3352c9dd0944bdae364",
|
||||
"phases": {
|
||||
"1": "Ideia e viabilidade",
|
||||
"2": "Formaliza\u00e7\u00e3o e dossier",
|
||||
"3": "Licenciamento e terra",
|
||||
"4": "Financiamento",
|
||||
"5": "Execu\u00e7\u00e3o e acompanhamento"
|
||||
},
|
||||
"steps": [
|
||||
{
|
||||
"code": "S01",
|
||||
"phase": 1,
|
||||
"title": "Sistematizar a ideia",
|
||||
"branch": null,
|
||||
"sequence": 1,
|
||||
"dependencies": [],
|
||||
"source_paragraph": 22,
|
||||
"id": "9f12bd89-308a-4e94-9cc3-df4822dea011",
|
||||
"version": 1,
|
||||
"state": "not_started",
|
||||
"saved_state": "not_started",
|
||||
"applicability": "applicable",
|
||||
"reason": "",
|
||||
"detail": {
|
||||
"state": "not_started"
|
||||
},
|
||||
"preparation_allowed": true,
|
||||
"action_gaps": [],
|
||||
"context_review_required": false
|
||||
},
|
||||
{
|
||||
"code": "S02",
|
||||
"phase": 1,
|
||||
"title": "An\u00e1lise preliminar",
|
||||
"branch": null,
|
||||
"sequence": 2,
|
||||
"dependencies": [],
|
||||
"source_paragraph": 23,
|
||||
"id": "369a3135-119c-42d2-83d2-6b5e3ff9e659",
|
||||
"version": 1,
|
||||
"state": "not_started",
|
||||
"saved_state": "not_started",
|
||||
"applicability": "applicable",
|
||||
"reason": "",
|
||||
"detail": {
|
||||
"state": "not_started"
|
||||
},
|
||||
"preparation_allowed": true,
|
||||
"action_gaps": [],
|
||||
"context_review_required": true
|
||||
},
|
||||
{
|
||||
"code": "S03",
|
||||
"phase": 1,
|
||||
"title": "Decis\u00e3o sobre a viabilidade",
|
||||
"branch": null,
|
||||
"sequence": 3,
|
||||
"dependencies": [],
|
||||
"source_paragraph": 24,
|
||||
"id": "0480e4d9-2265-4921-8338-83a0f728d49f",
|
||||
"version": 1,
|
||||
"state": "not_started",
|
||||
"saved_state": "not_started",
|
||||
"applicability": "applicable",
|
||||
"reason": "",
|
||||
"detail": {
|
||||
"state": "not_started"
|
||||
},
|
||||
"preparation_allowed": true,
|
||||
"action_gaps": [],
|
||||
"context_review_required": true
|
||||
},
|
||||
{
|
||||
"code": "S04",
|
||||
"phase": 1,
|
||||
"title": "Estruturar o investimento",
|
||||
"branch": null,
|
||||
"sequence": 4,
|
||||
"dependencies": [],
|
||||
"source_paragraph": 25,
|
||||
"id": "882503f7-e30d-4c8d-b7ca-fb157bda2dc7",
|
||||
"version": 1,
|
||||
"state": "not_started",
|
||||
"saved_state": "not_started",
|
||||
"applicability": "applicable",
|
||||
"reason": "",
|
||||
"detail": {
|
||||
"state": "not_started"
|
||||
},
|
||||
"preparation_allowed": true,
|
||||
"action_gaps": [],
|
||||
"context_review_required": true
|
||||
},
|
||||
{
|
||||
"code": "S05",
|
||||
"phase": 2,
|
||||
"title": "Constitui\u00e7\u00e3o e registo",
|
||||
"branch": null,
|
||||
"sequence": 5,
|
||||
"dependencies": [],
|
||||
"source_paragraph": 26,
|
||||
"id": "c3c87f28-f26a-42d0-80e9-74b412ae2ca5",
|
||||
"version": 1,
|
||||
"state": "not_started",
|
||||
"saved_state": "not_started",
|
||||
"applicability": "applicable",
|
||||
"reason": "",
|
||||
"detail": {
|
||||
"state": "not_started"
|
||||
},
|
||||
"preparation_allowed": true,
|
||||
"action_gaps": [],
|
||||
"context_review_required": false
|
||||
},
|
||||
{
|
||||
"code": "S06",
|
||||
"phase": 2,
|
||||
"title": "Preparar o dossier",
|
||||
"branch": null,
|
||||
"sequence": 6,
|
||||
"dependencies": [],
|
||||
"source_paragraph": 27,
|
||||
"id": "57865d31-78f3-4961-a065-49efd4ff15e8",
|
||||
"version": 1,
|
||||
"state": "not_started",
|
||||
"saved_state": "not_started",
|
||||
"applicability": "applicable",
|
||||
"reason": "",
|
||||
"detail": {
|
||||
"state": "not_started"
|
||||
},
|
||||
"preparation_allowed": true,
|
||||
"action_gaps": [],
|
||||
"context_review_required": false
|
||||
},
|
||||
{
|
||||
"code": "S07",
|
||||
"phase": 2,
|
||||
"title": "Submeter quando aplic\u00e1vel",
|
||||
"branch": null,
|
||||
"sequence": 7,
|
||||
"dependencies": {
|
||||
"submit": [
|
||||
"S06"
|
||||
]
|
||||
},
|
||||
"source_paragraph": 28,
|
||||
"id": "18dd4fa9-c309-4031-88eb-f4c27ab69e47",
|
||||
"version": 1,
|
||||
"state": "not_started",
|
||||
"saved_state": "not_started",
|
||||
"applicability": "applicable",
|
||||
"reason": "",
|
||||
"detail": {
|
||||
"state": "not_started"
|
||||
},
|
||||
"preparation_allowed": true,
|
||||
"action_gaps": {
|
||||
"submit": [
|
||||
"S06"
|
||||
]
|
||||
},
|
||||
"context_review_required": false
|
||||
},
|
||||
{
|
||||
"code": "S08",
|
||||
"phase": 3,
|
||||
"title": "Licenciamento pr\u00e9vio",
|
||||
"branch": null,
|
||||
"sequence": 8,
|
||||
"dependencies": [],
|
||||
"source_paragraph": 29,
|
||||
"id": "65d81f1c-da8d-4f2b-af0d-a6a049cbe54b",
|
||||
"version": 1,
|
||||
"state": "not_started",
|
||||
"saved_state": "not_started",
|
||||
"applicability": "applicable",
|
||||
"reason": "",
|
||||
"detail": {
|
||||
"state": "not_started"
|
||||
},
|
||||
"preparation_allowed": true,
|
||||
"action_gaps": [],
|
||||
"context_review_required": true
|
||||
},
|
||||
{
|
||||
"code": "S09",
|
||||
"phase": 3,
|
||||
"title": "Necessidade de terra",
|
||||
"branch": null,
|
||||
"sequence": 9,
|
||||
"dependencies": [],
|
||||
"source_paragraph": 30,
|
||||
"id": "b1a99d72-5ca6-4cb1-8a0a-7e35f1890392",
|
||||
"version": 1,
|
||||
"state": "not_started",
|
||||
"saved_state": "not_started",
|
||||
"applicability": "applicable",
|
||||
"reason": "",
|
||||
"detail": {
|
||||
"state": "not_started"
|
||||
},
|
||||
"preparation_allowed": true,
|
||||
"action_gaps": [],
|
||||
"context_review_required": false
|
||||
},
|
||||
{
|
||||
"code": "S10",
|
||||
"phase": 3,
|
||||
"title": "Regulariza\u00e7\u00e3o da terra",
|
||||
"branch": "land_needed",
|
||||
"sequence": 10,
|
||||
"dependencies": [],
|
||||
"source_paragraph": 31,
|
||||
"id": "162637cf-d578-4a75-87b9-369996834843",
|
||||
"version": 1,
|
||||
"state": "not_started",
|
||||
"saved_state": "not_started",
|
||||
"applicability": "undetermined",
|
||||
"reason": "",
|
||||
"detail": {
|
||||
"state": "not_started"
|
||||
},
|
||||
"preparation_allowed": true,
|
||||
"action_gaps": [],
|
||||
"context_review_required": false
|
||||
},
|
||||
{
|
||||
"code": "S11",
|
||||
"phase": 3,
|
||||
"title": "Confirmar regulariza\u00e7\u00e3o aplic\u00e1vel",
|
||||
"branch": null,
|
||||
"sequence": 11,
|
||||
"dependencies": [],
|
||||
"source_paragraph": 32,
|
||||
"id": "2c7d86f2-7426-4c6c-8ed4-cae8b6e68c99",
|
||||
"version": 1,
|
||||
"state": "not_started",
|
||||
"saved_state": "not_started",
|
||||
"applicability": "applicable",
|
||||
"reason": "",
|
||||
"detail": {
|
||||
"state": "not_started"
|
||||
},
|
||||
"preparation_allowed": true,
|
||||
"action_gaps": [],
|
||||
"context_review_required": false
|
||||
},
|
||||
{
|
||||
"code": "S12",
|
||||
"phase": 4,
|
||||
"title": "Necessidade e estrutura de financiamento",
|
||||
"branch": null,
|
||||
"sequence": 12,
|
||||
"dependencies": [],
|
||||
"source_paragraph": 33,
|
||||
"id": "7bdb6b9b-324d-49e2-a133-5865b423c830",
|
||||
"version": 1,
|
||||
"state": "not_started",
|
||||
"saved_state": "not_started",
|
||||
"applicability": "applicable",
|
||||
"reason": "",
|
||||
"detail": {
|
||||
"state": "not_started"
|
||||
},
|
||||
"preparation_allowed": true,
|
||||
"action_gaps": [],
|
||||
"context_review_required": true
|
||||
},
|
||||
{
|
||||
"code": "S13",
|
||||
"phase": 4,
|
||||
"title": "Dilig\u00eancia e negocia\u00e7\u00e3o financeira",
|
||||
"branch": "external_funding",
|
||||
"sequence": 13,
|
||||
"dependencies": [],
|
||||
"source_paragraph": 34,
|
||||
"id": "60bdbf70-8bbb-4771-abda-4f82b3f3e53e",
|
||||
"version": 1,
|
||||
"state": "not_started",
|
||||
"saved_state": "not_started",
|
||||
"applicability": "undetermined",
|
||||
"reason": "",
|
||||
"detail": {
|
||||
"state": "not_started"
|
||||
},
|
||||
"preparation_allowed": true,
|
||||
"action_gaps": [],
|
||||
"context_review_required": true
|
||||
},
|
||||
{
|
||||
"code": "S14",
|
||||
"phase": 4,
|
||||
"title": "Decis\u00e3o do financiador",
|
||||
"branch": "external_funding",
|
||||
"sequence": 14,
|
||||
"dependencies": [],
|
||||
"source_paragraph": 35,
|
||||
"id": "69a00105-ed81-4679-a0de-536b995d6b3e",
|
||||
"version": 1,
|
||||
"state": "not_started",
|
||||
"saved_state": "not_started",
|
||||
"applicability": "undetermined",
|
||||
"reason": "",
|
||||
"detail": {
|
||||
"state": "not_started"
|
||||
},
|
||||
"preparation_allowed": true,
|
||||
"action_gaps": [],
|
||||
"context_review_required": true
|
||||
},
|
||||
{
|
||||
"code": "S15",
|
||||
"phase": 4,
|
||||
"title": "Fecho e desembolso",
|
||||
"branch": "external_funding",
|
||||
"sequence": 15,
|
||||
"dependencies": {
|
||||
"execute": [
|
||||
"S14"
|
||||
]
|
||||
},
|
||||
"source_paragraph": 36,
|
||||
"id": "4b70d527-bdfd-4792-9fd9-a3cc7c397f28",
|
||||
"version": 1,
|
||||
"state": "not_started",
|
||||
"saved_state": "not_started",
|
||||
"applicability": "undetermined",
|
||||
"reason": "",
|
||||
"detail": {
|
||||
"state": "not_started"
|
||||
},
|
||||
"preparation_allowed": true,
|
||||
"action_gaps": {
|
||||
"execute": [
|
||||
"S14"
|
||||
]
|
||||
},
|
||||
"context_review_required": true
|
||||
},
|
||||
{
|
||||
"code": "S16",
|
||||
"phase": 5,
|
||||
"title": "Implementa\u00e7\u00e3o",
|
||||
"branch": null,
|
||||
"sequence": 16,
|
||||
"dependencies": [],
|
||||
"source_paragraph": 37,
|
||||
"id": "9c838949-2537-4d81-b848-adf20be1b789",
|
||||
"version": 1,
|
||||
"state": "not_started",
|
||||
"saved_state": "not_started",
|
||||
"applicability": "applicable",
|
||||
"reason": "",
|
||||
"detail": {
|
||||
"state": "not_started"
|
||||
},
|
||||
"preparation_allowed": true,
|
||||
"action_gaps": [],
|
||||
"context_review_required": false
|
||||
},
|
||||
{
|
||||
"code": "S17",
|
||||
"phase": 5,
|
||||
"title": "Inspec\u00e7\u00f5es, testes e licen\u00e7a de opera\u00e7\u00e3o",
|
||||
"branch": null,
|
||||
"sequence": 17,
|
||||
"dependencies": [],
|
||||
"source_paragraph": 38,
|
||||
"id": "8864abd3-fc9a-40f8-ad5f-22a84d87e10a",
|
||||
"version": 1,
|
||||
"state": "not_started",
|
||||
"saved_state": "not_started",
|
||||
"applicability": "applicable",
|
||||
"reason": "",
|
||||
"detail": {
|
||||
"state": "not_started"
|
||||
},
|
||||
"preparation_allowed": true,
|
||||
"action_gaps": [],
|
||||
"context_review_required": true
|
||||
},
|
||||
{
|
||||
"code": "S18",
|
||||
"phase": 5,
|
||||
"title": "In\u00edcio de opera\u00e7\u00e3o",
|
||||
"branch": null,
|
||||
"sequence": 18,
|
||||
"dependencies": {
|
||||
"operate": [
|
||||
"S17"
|
||||
]
|
||||
},
|
||||
"source_paragraph": 39,
|
||||
"id": "dc259b0e-2c14-422d-8309-dbd817f17ca4",
|
||||
"version": 1,
|
||||
"state": "not_started",
|
||||
"saved_state": "not_started",
|
||||
"applicability": "applicable",
|
||||
"reason": "",
|
||||
"detail": {
|
||||
"state": "not_started"
|
||||
},
|
||||
"preparation_allowed": true,
|
||||
"action_gaps": {
|
||||
"operate": [
|
||||
"S17"
|
||||
]
|
||||
},
|
||||
"context_review_required": false
|
||||
},
|
||||
{
|
||||
"code": "S19",
|
||||
"phase": 5,
|
||||
"title": "Acompanhamento recorrente",
|
||||
"branch": null,
|
||||
"sequence": 19,
|
||||
"dependencies": [],
|
||||
"source_paragraph": 40,
|
||||
"id": "22afbc5a-daec-473b-bd69-d90605d6d604",
|
||||
"version": 1,
|
||||
"state": "not_started",
|
||||
"saved_state": "not_started",
|
||||
"applicability": "applicable",
|
||||
"reason": "",
|
||||
"detail": {
|
||||
"state": "not_started"
|
||||
},
|
||||
"preparation_allowed": true,
|
||||
"action_gaps": [],
|
||||
"context_review_required": false
|
||||
}
|
||||
],
|
||||
"facts": {
|
||||
"land_needed": "unknown",
|
||||
"external_funding": "unknown",
|
||||
"viability": "unknown"
|
||||
},
|
||||
"fact_reasons": [],
|
||||
"context_opportunity": null,
|
||||
"opportunity_options": [],
|
||||
"fintech": null,
|
||||
"canWrite": true,
|
||||
"canReview": false,
|
||||
"next": {
|
||||
"code": "S01",
|
||||
"title": "Sistematizar a ideia",
|
||||
"action": "Preparar",
|
||||
"href": "#S01"
|
||||
},
|
||||
"investment_context": {
|
||||
"opportunity_choices": [],
|
||||
"preparation_documents": [],
|
||||
"preparation_snapshot": null,
|
||||
"revision": 1,
|
||||
"selection": {
|
||||
"opportunity": {
|
||||
"key": "opportunity:4",
|
||||
"kind": "opportunity",
|
||||
"id": "4",
|
||||
"title": "Synthetic acquisition acceptance",
|
||||
"version": 5,
|
||||
"href": "\/minhas-oportunidades\/ofertas\/4",
|
||||
"fields": {
|
||||
"project_title": "Synthetic acquisition acceptance",
|
||||
"project_summary": "Existing company description to reuse once in investment preparation.",
|
||||
"province": "Synthetic province",
|
||||
"province_id": "22fd440c-f744-4647-86a6-348c01c194d7",
|
||||
"location": "Synthetic province"
|
||||
},
|
||||
"documents": [],
|
||||
"visibility": "private",
|
||||
"state": "approved",
|
||||
"sha256": "b175039f4c49e40278436403a6aea19f218e03c3d9cb602583f0394caecd1f4e"
|
||||
}
|
||||
},
|
||||
"fields": {
|
||||
"project_title": "Synthetic acquisition acceptance",
|
||||
"investment_regime": "general",
|
||||
"special_zone_id": "",
|
||||
"currency": "MZN",
|
||||
"project_summary": "Existing company description to reuse once in investment preparation.",
|
||||
"province": "Synthetic province",
|
||||
"province_id": "22fd440c-f744-4647-86a6-348c01c194d7",
|
||||
"location": "Synthetic province"
|
||||
},
|
||||
"provenance": {
|
||||
"project_title": {
|
||||
"kind": "dossier",
|
||||
"id": "f71a2833-c182-43ba-862d-234e4b64739b",
|
||||
"version": 2
|
||||
},
|
||||
"currency": {
|
||||
"kind": "process_declaration",
|
||||
"actor_id": 8,
|
||||
"context_revision": 1
|
||||
},
|
||||
"project_summary": {
|
||||
"key": "opportunity:4",
|
||||
"kind": "opportunity",
|
||||
"id": "4",
|
||||
"title": "Synthetic acquisition acceptance",
|
||||
"version": 5,
|
||||
"href": "\/minhas-oportunidades\/ofertas\/4",
|
||||
"documents": [],
|
||||
"visibility": "private",
|
||||
"state": "approved",
|
||||
"sha256": "b175039f4c49e40278436403a6aea19f218e03c3d9cb602583f0394caecd1f4e"
|
||||
},
|
||||
"province": {
|
||||
"key": "opportunity:4",
|
||||
"kind": "opportunity",
|
||||
"id": "4",
|
||||
"title": "Synthetic acquisition acceptance",
|
||||
"version": 5,
|
||||
"href": "\/minhas-oportunidades\/ofertas\/4",
|
||||
"documents": [],
|
||||
"visibility": "private",
|
||||
"state": "approved",
|
||||
"sha256": "b175039f4c49e40278436403a6aea19f218e03c3d9cb602583f0394caecd1f4e"
|
||||
},
|
||||
"province_id": {
|
||||
"key": "opportunity:4",
|
||||
"kind": "opportunity",
|
||||
"id": "4",
|
||||
"title": "Synthetic acquisition acceptance",
|
||||
"version": 5,
|
||||
"href": "\/minhas-oportunidades\/ofertas\/4",
|
||||
"documents": [],
|
||||
"visibility": "private",
|
||||
"state": "approved",
|
||||
"sha256": "b175039f4c49e40278436403a6aea19f218e03c3d9cb602583f0394caecd1f4e"
|
||||
},
|
||||
"location": {
|
||||
"key": "opportunity:4",
|
||||
"kind": "opportunity",
|
||||
"id": "4",
|
||||
"title": "Synthetic acquisition acceptance",
|
||||
"version": 5,
|
||||
"href": "\/minhas-oportunidades\/ofertas\/4",
|
||||
"documents": [],
|
||||
"visibility": "private",
|
||||
"state": "approved",
|
||||
"sha256": "b175039f4c49e40278436403a6aea19f218e03c3d9cb602583f0394caecd1f4e"
|
||||
}
|
||||
},
|
||||
"review_steps": {
|
||||
"S02": {
|
||||
"revision": 1,
|
||||
"reason": "O contexto mudou; reveja os pressupostos e a evid\u00eancia aplic\u00e1vel."
|
||||
},
|
||||
"S03": {
|
||||
"revision": 1,
|
||||
"reason": "O contexto mudou; reveja os pressupostos e a evid\u00eancia aplic\u00e1vel."
|
||||
},
|
||||
"S04": {
|
||||
"revision": 1,
|
||||
"reason": "O contexto mudou; reveja os pressupostos e a evid\u00eancia aplic\u00e1vel."
|
||||
},
|
||||
"S08": {
|
||||
"revision": 1,
|
||||
"reason": "O contexto mudou; reveja os pressupostos e a evid\u00eancia aplic\u00e1vel."
|
||||
},
|
||||
"S12": {
|
||||
"revision": 1,
|
||||
"reason": "O contexto mudou; reveja os pressupostos e a evid\u00eancia aplic\u00e1vel."
|
||||
},
|
||||
"S13": {
|
||||
"revision": 1,
|
||||
"reason": "O contexto mudou; reveja os pressupostos e a evid\u00eancia aplic\u00e1vel."
|
||||
},
|
||||
"S14": {
|
||||
"revision": 1,
|
||||
"reason": "O contexto mudou; reveja os pressupostos e a evid\u00eancia aplic\u00e1vel."
|
||||
},
|
||||
"S15": {
|
||||
"revision": 1,
|
||||
"reason": "O contexto mudou; reveja os pressupostos e a evid\u00eancia aplic\u00e1vel."
|
||||
},
|
||||
"S17": {
|
||||
"revision": 1,
|
||||
"reason": "O contexto mudou; reveja os pressupostos e a evid\u00eancia aplic\u00e1vel."
|
||||
}
|
||||
},
|
||||
"dependency_hash": "1311d8539ea6a08237003a36712145618834461dd63b510658b5419e95af46dd",
|
||||
"updated_at": "2026-11-14T16:35:56+00:00",
|
||||
"updated_by": 8,
|
||||
"requirements_assessment": {
|
||||
"version": "context-requirements-20261023",
|
||||
"requirements": [],
|
||||
"fiscal_notice": "Localiza\u00e7\u00e3o, corredor, cluster ou projecto \u00e2ncora n\u00e3o activam benef\u00edcios fiscais. O simulador mant\u00e9m as condi\u00e7\u00f5es, fonte, per\u00edodo e \u00e2mbito pr\u00f3prios; condi\u00e7\u00f5es ausentes permanecem indeterminadas."
|
||||
},
|
||||
"complements": []
|
||||
},
|
||||
"results": [
|
||||
{
|
||||
"id": "a18f1c7a-73bd-41d1-a28b-6c19b1bf3fb8",
|
||||
"source_key": "3b31db9e28c3d4b33f05812d6292408541b24e06f720861cbea1350cd9b0b1c2",
|
||||
"draft_id": "38f76a31-44ea-4815-b523-d99046475518",
|
||||
"draft_version": 1,
|
||||
"snapshot_sha256": "aa09f1d3ea78bbc8eccd5336548bd39466946799030e5459fe1b42e89587d87b",
|
||||
"financial_snapshot": "{\"version\":1,\"data\":{\"budgetPeriod\":\"Mensal\",\"loan\":{\"principal\":\"100000\",\"rate\":\"0\",\"months\":\"24\",\"rateType\":\"nominal\",\"periodMonths\":\"1\",\"gracePeriods\":\"0\",\"upfrontFee\":\"0\",\"recurringFee\":\"0\"},\"breakEven\":{\"fixed\":\"3000\",\"price\":\"100\",\"variable\":\"50\"},\"funding\":{\"total\":\"100000\",\"secured\":\"30000\"},\"incomes\":[{\"label\":\"Synthetic scenario revenue\",\"amount\":\"10000\"}],\"expenses\":[{\"label\":\"Declared rent at synthetic province\",\"amount\":\"2000\"},{\"label\":\"Declared transport at synthetic province\",\"amount\":\"1000\"}]}}",
|
||||
"context_opportunity": null,
|
||||
"currency": "MZN",
|
||||
"engine": "financial-calculators:v1",
|
||||
"kind": "local_calculation_inputs",
|
||||
"context_state": "current",
|
||||
"context_revision": 1,
|
||||
"context_dependency_hash": "1311d8539ea6a08237003a36712145618834461dd63b510658b5419e95af46dd",
|
||||
"recorded_at": "2026-11-14T16:35:59+00:00"
|
||||
}
|
||||
],
|
||||
"requests": [],
|
||||
"documents": [],
|
||||
"fiscal_reports": [],
|
||||
"land_report": null,
|
||||
"preparation_report": null,
|
||||
"company_reports": [],
|
||||
"zone_contexts": [],
|
||||
"sector_contexts": [],
|
||||
"sector_references": [],
|
||||
"content_sources": [],
|
||||
"tasks": [],
|
||||
"services": [],
|
||||
"tools": {
|
||||
"funding": "\/fundos?kind=institution&location_mode=physical&context=f71a2833-c182-43ba-862d-234e4b64739b",
|
||||
"simulator": "\/simulador-fiscal?context=f71a2833-c182-43ba-862d-234e4b64739b&draft=38f76a31-44ea-4815-b523-d99046475518",
|
||||
"preparation": null,
|
||||
"company": "\/investimentos\/f71a2833-c182-43ba-862d-234e4b64739b\/forma-juridica",
|
||||
"desk": "\/balcao-virtual\/dossiers\/f71a2833-c182-43ba-862d-234e4b64739b",
|
||||
"guidance": "\/balcao-virtual\/dossiers\/f71a2833-c182-43ba-862d-234e4b64739b\/orientacao",
|
||||
"library": "\/documentos?context=f71a2833-c182-43ba-862d-234e4b64739b",
|
||||
"opportunities": "\/oportunidades?context=f71a2833-c182-43ba-862d-234e4b64739b",
|
||||
"territory": "\/zonas?context=f71a2833-c182-43ba-862d-234e4b64739b",
|
||||
"partners": "\/parcerias\/painel?context=f71a2833-c182-43ba-862d-234e4b64739b",
|
||||
"land": "\/investidor\/terra-duat\/novo?context=f71a2833-c182-43ba-862d-234e4b64739b"
|
||||
}
|
||||
},
|
||||
"saved": {
|
||||
"project_title": "Synthetic acquisition acceptance",
|
||||
"currency": "MZN",
|
||||
"project_summary": "Existing company description to reuse once in investment preparation.",
|
||||
"province_id": "22fd440c-f744-4647-86a6-348c01c194d7",
|
||||
"journey_type": "investor",
|
||||
"purpose": "simulate_only",
|
||||
"context_applicability_snapshot": "{\"version\":1,\"context_revision\":1,\"consumer_opportunity\":null,\"portfolio_origin\":null,\"zone\":null,\"provenance\":{\"project_title\":{\"kind\":\"dossier\",\"id\":\"f71a2833-c182-43ba-862d-234e4b64739b\",\"version\":2},\"currency\":{\"kind\":\"process_declaration\",\"actor_id\":8,\"context_revision\":1},\"project_summary\":{\"key\":\"opportunity:4\",\"kind\":\"opportunity\",\"id\":\"4\",\"title\":\"Synthetic acquisition acceptance\",\"version\":5,\"href\":\"\\\/minhas-oportunidades\\\/ofertas\\\/4\",\"documents\":[],\"visibility\":\"private\",\"state\":\"approved\",\"sha256\":\"b175039f4c49e40278436403a6aea19f218e03c3d9cb602583f0394caecd1f4e\"},\"province\":{\"key\":\"opportunity:4\",\"kind\":\"opportunity\",\"id\":\"4\",\"title\":\"Synthetic acquisition acceptance\",\"version\":5,\"href\":\"\\\/minhas-oportunidades\\\/ofertas\\\/4\",\"documents\":[],\"visibility\":\"private\",\"state\":\"approved\",\"sha256\":\"b175039f4c49e40278436403a6aea19f218e03c3d9cb602583f0394caecd1f4e\"},\"province_id\":{\"key\":\"opportunity:4\",\"kind\":\"opportunity\",\"id\":\"4\",\"title\":\"Synthetic acquisition acceptance\",\"version\":5,\"href\":\"\\\/minhas-oportunidades\\\/ofertas\\\/4\",\"documents\":[],\"visibility\":\"private\",\"state\":\"approved\",\"sha256\":\"b175039f4c49e40278436403a6aea19f218e03c3d9cb602583f0394caecd1f4e\"},\"location\":{\"key\":\"opportunity:4\",\"kind\":\"opportunity\",\"id\":\"4\",\"title\":\"Synthetic acquisition acceptance\",\"version\":5,\"href\":\"\\\/minhas-oportunidades\\\/ofertas\\\/4\",\"documents\":[],\"visibility\":\"private\",\"state\":\"approved\",\"sha256\":\"b175039f4c49e40278436403a6aea19f218e03c3d9cb602583f0394caecd1f4e\"}},\"opportunity_choices\":[],\"complements\":[],\"requirements_assessment\":{\"version\":\"context-requirements-20261023\",\"requirements\":[],\"fiscal_notice\":\"Localiza\u00e7\u00e3o, corredor, cluster ou projecto \u00e2ncora n\u00e3o activam benef\u00edcios fiscais. O simulador mant\u00e9m as condi\u00e7\u00f5es, fonte, per\u00edodo e \u00e2mbito pr\u00f3prios; condi\u00e7\u00f5es ausentes permanecem indeterminadas.\"},\"path\":{\"definition_id\":\"61000919-0000-4000-8000-000000000001\",\"definition_version\":1,\"source_sha256\":\"aa13c53138ec15cd808de43d9dc2fc6808da86943d09d3352c9dd0944bdae364\",\"steps\":[{\"code\":\"S10\",\"title\":\"Regulariza\u00e7\u00e3o da terra\",\"state\":\"not_started\",\"applicability\":\"undetermined\",\"reason\":\"\",\"context_review_required\":false},{\"code\":\"S13\",\"title\":\"Dilig\u00eancia e negocia\u00e7\u00e3o financeira\",\"state\":\"not_started\",\"applicability\":\"undetermined\",\"reason\":\"\",\"context_review_required\":true},{\"code\":\"S14\",\"title\":\"Decis\u00e3o do financiador\",\"state\":\"not_started\",\"applicability\":\"undetermined\",\"reason\":\"\",\"context_review_required\":true},{\"code\":\"S15\",\"title\":\"Fecho e desembolso\",\"state\":\"not_started\",\"applicability\":\"undetermined\",\"reason\":\"\",\"context_review_required\":true}]},\"land_preparation\":{\"need\":\"unknown\",\"reason\":\"\"},\"field_provenance\":{\"project_title\":{\"kind\":\"dossier\",\"version\":2},\"currency\":{\"kind\":\"process_declaration\",\"context_revision\":1},\"project_summary\":{\"kind\":\"opportunity\",\"title\":\"Synthetic acquisition acceptance\",\"version\":5},\"province\":{\"kind\":\"opportunity\",\"title\":\"Synthetic acquisition acceptance\",\"version\":5},\"province_id\":{\"kind\":\"opportunity\",\"title\":\"Synthetic acquisition acceptance\",\"version\":5},\"location\":{\"kind\":\"opportunity\",\"title\":\"Synthetic acquisition acceptance\",\"version\":5}},\"preparation\":null,\"fields\":{\"investment_regime\":\"general\",\"currency\":\"MZN\"},\"notice\":\"Refer\u00eancias e declara\u00e7\u00f5es para prepara\u00e7\u00e3o; sem reconhecimento autom\u00e1tico de elegibilidade ou benef\u00edcio.\"}",
|
||||
"financial_tools_snapshot": "{\"version\":1,\"data\":{\"budgetPeriod\":\"Mensal\",\"loan\":{\"principal\":\"100000\",\"rate\":\"0\",\"months\":\"24\",\"rateType\":\"nominal\",\"periodMonths\":\"1\",\"gracePeriods\":\"0\",\"upfrontFee\":\"0\",\"recurringFee\":\"0\"},\"breakEven\":{\"fixed\":\"3000\",\"price\":\"100\",\"variable\":\"50\"},\"funding\":{\"total\":\"100000\",\"secured\":\"30000\"},\"incomes\":[{\"label\":\"Synthetic scenario revenue\",\"amount\":\"10000\"}],\"expenses\":[{\"label\":\"Declared rent at synthetic province\",\"amount\":\"2000\"},{\"label\":\"Declared transport at synthetic province\",\"amount\":\"1000\"}]}}"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"scope": "Synthetic investor-declared costs at the selected location; no automatic profitability or fiscal entitlement",
|
||||
"province_id": "22fd440c-f744-4647-86a6-348c01c194d7",
|
||||
"budget": {
|
||||
"incomeTotal": "10000.00",
|
||||
"expenseTotal": "3000.00",
|
||||
"operatingBalance": "7000.00",
|
||||
"status": "estimated"
|
||||
},
|
||||
"breakEven": {
|
||||
"contributionPerUnit": "50.00",
|
||||
"breakEvenUnits": "60",
|
||||
"breakEvenRevenue": "6000.00",
|
||||
"status": "estimated"
|
||||
},
|
||||
"alternative": {
|
||||
"incomeTotal": "10000.00",
|
||||
"expenseTotal": "8000.00",
|
||||
"operatingBalance": "2000.00",
|
||||
"status": "estimated"
|
||||
},
|
||||
"source_offer": "opportunity:4",
|
||||
"case_id": "f71a2833-c182-43ba-862d-234e4b64739b"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"tool":"phpstan","result":"passed","errors":0}
|
||||
Binary file not shown.
File diff suppressed because one or more lines are too long
@@ -0,0 +1,102 @@
|
||||
{
|
||||
"base": "ed2fd86f",
|
||||
"phase5_preserved": "528a2597",
|
||||
"environment": {
|
||||
"database": "offers_phase1_20261114_test",
|
||||
"host": "127.0.0.1",
|
||||
"port": 55441,
|
||||
"fixtures": "synthetic only"
|
||||
},
|
||||
"cause": "Transformation: description persisted, project_summary read only canonical/summary. Existing identifiers and permissions preserved.",
|
||||
"php_suites": [
|
||||
{
|
||||
"name": "Tests\\Feature\\OfferInvestmentAcceptanceAuditTest",
|
||||
"tests": 4,
|
||||
"assertions": 62,
|
||||
"errors": 0,
|
||||
"failures": 0
|
||||
},
|
||||
{
|
||||
"name": "Tests\\Feature\\PrivateOfferConfidentialityTest",
|
||||
"tests": 11,
|
||||
"assertions": 161,
|
||||
"errors": 0,
|
||||
"failures": 0
|
||||
},
|
||||
{
|
||||
"name": "Tests\\Feature\\InvestorPathTest",
|
||||
"tests": 4,
|
||||
"assertions": 192,
|
||||
"errors": 0,
|
||||
"failures": 0
|
||||
},
|
||||
{
|
||||
"name": "Tests\\Feature\\OpportunityManagementTest",
|
||||
"tests": 3,
|
||||
"assertions": 46,
|
||||
"errors": 0,
|
||||
"failures": 0
|
||||
}
|
||||
],
|
||||
"php_totals": {
|
||||
"tests": 22,
|
||||
"assertions": 461,
|
||||
"errors": 0,
|
||||
"failures": 0
|
||||
},
|
||||
"frontend_regression": {
|
||||
"tests": 41,
|
||||
"passed": 41
|
||||
},
|
||||
"integrated_frontend_report": {
|
||||
"tests": 1,
|
||||
"passed": 1,
|
||||
"artifacts": [
|
||||
"private-integrated-report.html",
|
||||
"private-integrated-report.docx",
|
||||
"location-analysis.json"
|
||||
]
|
||||
},
|
||||
"static": {
|
||||
"phpstan": "full analysis passed, zero errors",
|
||||
"types": "tsc --noEmit passed",
|
||||
"pint": "affected PHP passed",
|
||||
"frontend_lint_format": "new mjs passed",
|
||||
"build": "unchanged frontend product sources: accepted QA build reused; no rebuild",
|
||||
"git_diff_check": "passed"
|
||||
},
|
||||
"field_policy": "Common authorised title/description/administrative references only; no private_offer financial/contact/price/proof transfer. NDA grant does not grant private context access.",
|
||||
"publication": "default consumer closed; injected synthetic consumer guarded by actual isolated database",
|
||||
"matrix": "RF-MN-003 only; other rows unchanged",
|
||||
"pending": [
|
||||
"real NDA and legal decisions",
|
||||
"effective public writer/activation",
|
||||
"desktop/mobile visual acceptance",
|
||||
"image processing derivatives",
|
||||
"real email delivery"
|
||||
],
|
||||
"installations": false,
|
||||
"operational_migrations": false,
|
||||
"mne_changes": false,
|
||||
"accepted_59_780_repeated": false,
|
||||
"source_sha256": {
|
||||
"app/Services/InvestorContext.php": "cdae33a63d796b7e9862c949711b2c83f2482990d3d77806591a594214313fff",
|
||||
"tests/Feature/OfferInvestmentAcceptanceAuditTest.php": "d91112cfccc08f45afb9a6c317ead1a5d03f97221a606ad123ca590a4b401b5b",
|
||||
"tests/Feature/PrivateOfferConfidentialityTest.php": "5beb77c609baf3c78768bfbe0906b5d5b8701cd3266ca9ead8c3198a30fb5bc6",
|
||||
"tests/frontend/offer-investment-continuity.test.mjs": "b37acc9d1dbf900aa8a5555666142bc088e73245b60c5aa8f6d0f7baa1a7655a"
|
||||
},
|
||||
"evidence_sha256": {
|
||||
"focused.log": "0943af2982f42d60848e480008f0e045ebda4da944d16408be0ae944b94428bc",
|
||||
"focused.xml": "e57322452c4e009e40dc0b3d5bbfa1da1c3add6afb74a8006143efbc904ee2cb",
|
||||
"frontend-regression.log": "ddbeb6fe3365cb70fd4c0e5eafaf4d7919ead5910415f6aed9167acfb43e315d",
|
||||
"integrated-report-input.json": "60d8d8f46821da56f47183f57e6869cb64160cf5f5c2fdd03eab2acdd24c5014",
|
||||
"location-analysis.json": "83729ad333ace4507e3827b97f84905327083682fb83eecbb05847e4f59a5dff",
|
||||
"phpstan.log": "c10f74fb840de989bba7e9c2dfee7f8419b5d2ac0cc0daf18a4a187f6530aa43",
|
||||
"private-integrated-report.docx": "df76a52f6f1d1ee990702aefa8b4c3d3604de2305be840be88c77fc3dbdf546e",
|
||||
"private-integrated-report.html": "3221f94d4df400f4edf5e9b262dbf4a13d6f80d6b6fd58b6f8a09ed384141367",
|
||||
"regression.log": "13480527b87e724e7c9687131ee0d0f611f1c9a6ac719612440dcfac8ccfd543",
|
||||
"regression.xml": "2785f34fb05e01b945fb1b7a61ba3e9d6c061d5c7d3cf09b189188b58a1b2e37",
|
||||
"report-test.log": "71d866136fca227770e0c73347750f6597224c9420672c6ab9dfcebd9f80c60d",
|
||||
"types.log": "b59cdb3ff539a29c8ae6c37772ecc1a93a5266c49959d408f16b17487b7ac755"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"tool":"phpunit","result":"passed","tests":22,"passed":22,"assertions":461,"duration_ms":135835}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<testsuites>
|
||||
<testsuite name="CLI Arguments" tests="22" assertions="461" errors="0" failures="0" skipped="0" time="135.825495">
|
||||
<testsuite name="Tests\Feature\OfferInvestmentAcceptanceAuditTest" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\OfferInvestmentAcceptanceAuditTest.php" tests="4" assertions="62" errors="0" failures="0" skipped="0" time="28.758459">
|
||||
<testcase name="test_saved_offer_reuses_its_description_in_the_existing_investment_context" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\OfferInvestmentAcceptanceAuditTest.php" line="41" class="Tests\Feature\OfferInvestmentAcceptanceAuditTest" classname="Tests.Feature.OfferInvestmentAcceptanceAuditTest" assertions="8" time="6.650455"/>
|
||||
<testcase name="test_native_context_options_do_not_disclose_another_owners_private_offer" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\OfferInvestmentAcceptanceAuditTest.php" line="55" class="Tests\Feature\OfferInvestmentAcceptanceAuditTest" classname="Tests.Feature.OfferInvestmentAcceptanceAuditTest" assertions="7" time="2.800221"/>
|
||||
<testcase name="test_private_description_is_not_truncated_and_does_not_replace_an_existing_summary" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\OfferInvestmentAcceptanceAuditTest.php" line="67" class="Tests\Feature\OfferInvestmentAcceptanceAuditTest" classname="Tests.Feature.OfferInvestmentAcceptanceAuditTest" assertions="5" time="2.095158"/>
|
||||
<testcase name="test_approved_offer_continues_in_same_dossier_location_analysis_saved_result_and_report_input" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\OfferInvestmentAcceptanceAuditTest.php" line="80" class="Tests\Feature\OfferInvestmentAcceptanceAuditTest" classname="Tests.Feature.OfferInvestmentAcceptanceAuditTest" assertions="42" time="17.212625"/>
|
||||
</testsuite>
|
||||
<testsuite name="Tests\Feature\PrivateOfferConfidentialityTest" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\PrivateOfferConfidentialityTest.php" tests="11" assertions="161" errors="0" failures="0" skipped="0" time="100.821485">
|
||||
<testcase name="test_confirmed_specific_fields_save_recover_and_bind_the_editorial_version" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\PrivateOfferConfidentialityTest.php" line="109" class="Tests\Feature\PrivateOfferConfidentialityTest" classname="Tests.Feature.PrivateOfferConfidentialityTest" assertions="9" time="7.598624"/>
|
||||
<testcase name="test_property_fields_are_optional_and_categories_and_unknown_fields_are_enforced" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\PrivateOfferConfidentialityTest.php" line="120" class="Tests\Feature\PrivateOfferConfidentialityTest" classname="Tests.Feature.PrivateOfferConfidentialityTest" assertions="7" time="5.093540"/>
|
||||
<testcase name="test_six_photos_limit_includes_restoration_and_alternate_routes_and_cleans_failed_uploads" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\PrivateOfferConfidentialityTest.php" line="131" class="Tests\Feature\PrivateOfferConfidentialityTest" classname="Tests.Feature.PrivateOfferConfidentialityTest" assertions="15" time="10.676252"/>
|
||||
<testcase name="test_upload_validation_and_quarantine_do_not_grant_publication_or_read_access" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\PrivateOfferConfidentialityTest.php" line="173" class="Tests\Feature\PrivateOfferConfidentialityTest" classname="Tests.Feature.PrivateOfferConfidentialityTest" assertions="9" time="7.387037"/>
|
||||
<testcase name="test_proof_bytes_are_encrypted_and_only_a_scoped_internal_validator_can_read_them" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\PrivateOfferConfidentialityTest.php" line="187" class="Tests\Feature\PrivateOfferConfidentialityTest" classname="Tests.Feature.PrivateOfferConfidentialityTest" assertions="14" time="5.185126"/>
|
||||
<testcase name="test_nda_absence_keeps_access_closed_and_acceptance_alone_never_grants_access" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\PrivateOfferConfidentialityTest.php" line="209" class="Tests\Feature\PrivateOfferConfidentialityTest" classname="Tests.Feature.PrivateOfferConfidentialityTest" assertions="13" time="8.880152"/>
|
||||
<testcase name="test_both_conditions_exact_versions_revocation_and_audit_are_required_on_every_read" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\PrivateOfferConfidentialityTest.php" line="227" class="Tests\Feature\PrivateOfferConfidentialityTest" classname="Tests.Feature.PrivateOfferConfidentialityTest" assertions="18" time="9.641431"/>
|
||||
<testcase name="test_nda_version_changes_withdrawal_session_revocation_and_private_revision_close_access" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\PrivateOfferConfidentialityTest.php" line="259" class="Tests\Feature\PrivateOfferConfidentialityTest" classname="Tests.Feature.PrivateOfferConfidentialityTest" assertions="35" time="21.375187"/>
|
||||
<testcase name="test_nda_integrity_and_authority_loss_are_rechecked_without_exposing_the_company" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\PrivateOfferConfidentialityTest.php" line="299" class="Tests\Feature\PrivateOfferConfidentialityTest" classname="Tests.Feature.PrivateOfferConfidentialityTest" assertions="14" time="9.466717"/>
|
||||
<testcase name="test_compiled_confidential_page_has_no_company_data_before_both_conditions" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\PrivateOfferConfidentialityTest.php" line="323" class="Tests\Feature\PrivateOfferConfidentialityTest" classname="Tests.Feature.PrivateOfferConfidentialityTest" assertions="11" time="6.025318"/>
|
||||
<testcase name="test_photo_metadata_and_original_are_bound_to_the_editorial_fingerprint" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\PrivateOfferConfidentialityTest.php" line="337" class="Tests\Feature\PrivateOfferConfidentialityTest" classname="Tests.Feature.PrivateOfferConfidentialityTest" assertions="16" time="9.492101"/>
|
||||
</testsuite>
|
||||
<testsuite name="Tests\Feature\InvestorPathTest" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\InvestorPathTest.php" tests="4" assertions="192" errors="0" failures="0" skipped="0" time="4.861569">
|
||||
<testcase name="test_existing_empty_desk_draft_can_explicitly_reuse_current_contextual_summary_without_overwriting_data" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\InvestorPathTest.php" line="53" class="Tests\Feature\InvestorPathTest" classname="Tests.Feature.InvestorPathTest" assertions="29" time="0.792451"/>
|
||||
<testcase name="test_two_independent_investments_recover_tool_evidence_support_and_internal_tasks_without_external_calls" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\InvestorPathTest.php" line="83" class="Tests\Feature\InvestorPathTest" classname="Tests.Feature.InvestorPathTest" assertions="107" time="2.252891"/>
|
||||
<testcase name="test_permissions_stale_versions_and_real_dependencies_do_not_block_independent_preparation" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\InvestorPathTest.php" line="131" class="Tests\Feature\InvestorPathTest" classname="Tests.Feature.InvestorPathTest" assertions="35" time="0.995810"/>
|
||||
<testcase name="test_applicability_changes_preserve_old_branch_evidence_and_legacy_workflow" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\InvestorPathTest.php" line="154" class="Tests\Feature\InvestorPathTest" classname="Tests.Feature.InvestorPathTest" assertions="21" time="0.820416"/>
|
||||
</testsuite>
|
||||
<testsuite name="Tests\Feature\OpportunityManagementTest" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\OpportunityManagementTest.php" tests="3" assertions="46" errors="0" failures="0" skipped="0" time="1.383981">
|
||||
<testcase name="test_installation_requires_management_permission_and_current_proofs" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\OpportunityManagementTest.php" line="20" class="Tests\Feature\OpportunityManagementTest" classname="Tests.Feature.OpportunityManagementTest" assertions="6" time="0.332180"/>
|
||||
<testcase name="test_incomplete_draft_recovers_and_soft_delete_restore_preserves_identity" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\OpportunityManagementTest.php" line="32" class="Tests\Feature\OpportunityManagementTest" classname="Tests.Feature.OpportunityManagementTest" assertions="21" time="0.720887"/>
|
||||
<testcase name="test_closed_version_edits_do_not_change_public_values_classification_or_relations" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\OpportunityManagementTest.php" line="60" class="Tests\Feature\OpportunityManagementTest" classname="Tests.Feature.OpportunityManagementTest" assertions="19" time="0.330915"/>
|
||||
</testsuite>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
@@ -0,0 +1,9 @@
|
||||
✔ persisted synthetic offer crosses the dossier boundary into location assumptions, calculation and private report (130.377542ms)
|
||||
ℹ tests 1
|
||||
ℹ suites 0
|
||||
ℹ pass 1
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 596.953708
|
||||
@@ -0,0 +1,3 @@
|
||||
|
||||
> types:check
|
||||
> tsc --noEmit
|
||||
@@ -0,0 +1,340 @@
|
||||
|
||||
> build
|
||||
> vp build
|
||||
|
||||
npm.cmd : [plugin laravel:fonts] Optimized font fallbacks require the optional "fontaine" package. Install it, or set
|
||||
"optimizedFallbacks: false" on your fonts to disable the feature.
|
||||
At line:2 char:30
|
||||
+ ... _BUILD='1'; npm.cmd run build *> outputs/offers-menu-qa-20261114/buil ...
|
||||
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
+ CategoryInfo : NotSpecified: ([plugin laravel...le the feature.:String) [], RemoteException
|
||||
+ FullyQualifiedErrorId : NativeCommandError
|
||||
|
||||
transforming...
|
||||
✓ 2849 modules transformed.
|
||||
rendering chunks...
|
||||
computing gzip size...
|
||||
public/build-qa-opportunities/assets/layers-BWBAp2CZ.png 0.69 kB
|
||||
public/build-qa-opportunities/assets/layers-2x-Bpkbi35X.png 1.25 kB
|
||||
public/build-qa-opportunities/assets/marker-icon-hN30_KVU.png 1.46 kB
|
||||
public/build-qa-opportunities/assets/noto-sans-400-normal-BTkUljjl.woff2 13.12 kB
|
||||
public/build-qa-opportunities/assets/noto-sans-700-normal-CrvTEWhy.woff2 13.46 kB
|
||||
public/build-qa-opportunities/assets/noto-sans-500-normal-DCA2Z9Cj.woff2 13.49 kB
|
||||
public/build-qa-opportunities/assets/noto-sans-600-normal-Dkgu5AE5.woff2 13.49 kB
|
||||
public/build-qa-opportunities/assets/noto-serif-400-normal-BV_iYGc9.woff2 14.40 kB
|
||||
public/build-qa-opportunities/assets/noto-serif-700-normal-CIw17UNa.woff2 15.23 kB
|
||||
public/build-qa-opportunities/assets/noto-serif-600-normal-BiuDA9L7.woff2 15.31 kB
|
||||
public/build-qa-opportunities/assets/noto-sans-400-normal-DPpKfCgK.woff 16.68 kB
|
||||
public/build-qa-opportunities/assets/instrument-sans-400-normal-DRC__1Mx.woff2 16.86 kB
|
||||
public/build-qa-opportunities/assets/noto-sans-700-normal-ZyrKABVF.woff 16.95 kB
|
||||
public/build-qa-opportunities/assets/noto-sans-500-normal-CDPngY5C.woff 17.09 kB
|
||||
public/build-qa-opportunities/assets/noto-sans-600-normal-DVJMj_t0.woff 17.10 kB
|
||||
public/build-qa-opportunities/assets/instrument-sans-500-normal-Dk9ku72i.woff2 17.23 kB
|
||||
public/build-qa-opportunities/assets/instrument-sans-600-normal-B7fBEWYG.woff2 17.40 kB
|
||||
public/build-qa-opportunities/assets/noto-serif-400-normal-DT0cwSa_.woff 17.81 kB
|
||||
public/build-qa-opportunities/fonts-manifest.json 18.53 kB │ gzip: 1.23 kB
|
||||
public/build-qa-opportunities/assets/noto-serif-700-normal-mX-AlFMT.woff 18.54 kB
|
||||
public/build-qa-opportunities/assets/noto-serif-600-normal-RZopCp-J.woff 18.68 kB
|
||||
public/build-qa-opportunities/assets/instrument-sans-400-normal-D1W7dsQl.woff 21.24 kB
|
||||
public/build-qa-opportunities/assets/instrument-sans-500-normal-Z6ESRlEs.woff 21.65 kB
|
||||
public/build-qa-opportunities/assets/instrument-sans-600-normal-B9e8oLYv.woff 21.67 kB
|
||||
public/build-qa-opportunities/manifest.json 116.89 kB │ gzip: 12.31 kB
|
||||
public/build-qa-opportunities/assets/fonts-CcFLQaQ-.css 7.91 kB │ gzip: 0.65 kB
|
||||
public/build-qa-opportunities/assets/investment-catalogue-WaRR19FV.css 8.15 kB │ gzip: 2.00 kB
|
||||
public/build-qa-opportunities/assets/interactive-investment-map-Bm3WjoQm.css 10.62 kB │ gzip: 2.65 kB
|
||||
public/build-qa-opportunities/assets/app-UznX-FqS.css 157.42 kB │ gzip: 25.28 kB
|
||||
public/build-qa-opportunities/assets/input-text-BlEFHnL-.js 0.09 kB │ gzip: 0.10 kB
|
||||
public/build-qa-opportunities/assets/check-PcBNn_NG.js 0.12 kB │ gzip: 0.14 kB
|
||||
public/build-qa-opportunities/assets/company-comparison-qLTgpXjz.js 0.12 kB │ gzip: 0.13 kB
|
||||
public/build-qa-opportunities/assets/chevron-down-Bjkqe1aH.js 0.12 kB │ gzip: 0.14 kB
|
||||
public/build-qa-opportunities/assets/chevron-right-K0fOEtTQ.js 0.12 kB │ gzip: 0.14 kB
|
||||
public/build-qa-opportunities/assets/display-value-DRR1EAAK.js 0.13 kB │ gzip: 0.12 kB
|
||||
public/build-qa-opportunities/assets/value-text-BztaYdGl.js 0.14 kB │ gzip: 0.12 kB
|
||||
public/build-qa-opportunities/assets/loader-circle-DS8Tf4er.js 0.14 kB │ gzip: 0.15 kB
|
||||
public/build-qa-opportunities/assets/shared-B-QpuOU2.js 0.15 kB │ gzip: 0.13 kB
|
||||
public/build-qa-opportunities/assets/arrow-left-DMVHIw8Z.js 0.16 kB │ gzip: 0.16 kB
|
||||
public/build-qa-opportunities/assets/arrow-up-right-BIVouzdL.js 0.16 kB │ gzip: 0.15 kB
|
||||
public/build-qa-opportunities/assets/search-BRvPeo47.js 0.17 kB │ gzip: 0.16 kB
|
||||
public/build-qa-opportunities/assets/circle-check-DckzuBoW.js 0.17 kB │ gzip: 0.16 kB
|
||||
public/build-qa-opportunities/assets/product-usage-CHhCL93_.js 0.18 kB │ gzip: 0.17 kB
|
||||
public/build-qa-opportunities/assets/editorial-states-Bc7NQzsj.js 0.19 kB │ gzip: 0.16 kB
|
||||
public/build-qa-opportunities/assets/types-BoD_IJFv.js 0.19 kB │ gzip: 0.16 kB
|
||||
public/build-qa-opportunities/assets/participation-identity-BWabb4yN.js 0.24 kB │ gzip: 0.18 kB
|
||||
public/build-qa-opportunities/assets/circle-alert-uV7omYjr.js 0.24 kB │ gzip: 0.18 kB
|
||||
public/build-qa-opportunities/assets/eye-CmcEBfxN.js 0.25 kB │ gzip: 0.20 kB
|
||||
public/build-qa-opportunities/assets/log-in-Wgc8VLfq.js 0.25 kB │ gzip: 0.22 kB
|
||||
public/build-qa-opportunities/assets/partnership-history-CfclSLAo.js 0.27 kB │ gzip: 0.19 kB
|
||||
public/build-qa-opportunities/assets/compiler-runtime-sKsOXjDo.js 0.27 kB │ gzip: 0.23 kB
|
||||
public/build-qa-opportunities/assets/file-text-CEJtuNfp.js 0.33 kB │ gzip: 0.21 kB
|
||||
public/build-qa-opportunities/assets/confirm-Culw9Q-x.js 0.36 kB │ gzip: 0.22 kB
|
||||
public/build-qa-opportunities/assets/text-link-BBvcP60s.js 0.40 kB │ gzip: 0.27 kB
|
||||
public/build-qa-opportunities/assets/content-management-entry-C_oY4VyW.js 0.42 kB │ gzip: 0.31 kB
|
||||
public/build-qa-opportunities/assets/handshake-YqyTTm3a.js 0.44 kB │ gzip: 0.28 kB
|
||||
public/build-qa-opportunities/assets/investment-guides-DO58L8zr.js 0.45 kB │ gzip: 0.26 kB
|
||||
public/build-qa-opportunities/assets/catalogue-context-Dj8DfFuR.js 0.47 kB │ gzip: 0.30 kB
|
||||
public/build-qa-opportunities/assets/operation-key-Dr4VtGxa.js 0.51 kB │ gzip: 0.35 kB
|
||||
public/build-qa-opportunities/assets/spinner-Dmx1j41V.js 0.52 kB │ gzip: 0.36 kB
|
||||
public/build-qa-opportunities/assets/types-Cxp_Nskt.js 0.55 kB │ gzip: 0.32 kB
|
||||
public/build-qa-opportunities/assets/sliders-horizontal-D-4FqAHA.js 0.58 kB │ gzip: 0.26 kB
|
||||
public/build-qa-opportunities/assets/page-preparation-guard-B-OckhBt.js 0.59 kB │ gzip: 0.35 kB
|
||||
public/build-qa-opportunities/assets/portal-presentation-fH7FShU8.js 0.62 kB │ gzip: 0.38 kB
|
||||
public/build-qa-opportunities/assets/confidentiality-C2eqbRBb.js 0.64 kB │ gzip: 0.41 kB
|
||||
public/build-qa-opportunities/assets/dist-RzQD0wUd.js 0.68 kB │ gzip: 0.43 kB
|
||||
public/build-qa-opportunities/assets/dist-qw3FPjRO.js 0.74 kB │ gzip: 0.49 kB
|
||||
public/build-qa-opportunities/assets/labels-DxqjesIG.js 0.74 kB │ gzip: 0.46 kB
|
||||
public/build-qa-opportunities/assets/wayfinder-Dp7PqxvV.js 0.89 kB │ gzip: 0.53 kB
|
||||
public/build-qa-opportunities/assets/createLucideIcon-oMeAlXbs.js 0.95 kB │ gzip: 0.56 kB
|
||||
public/build-qa-opportunities/assets/use-interest-draft-waHUBAew.js 0.99 kB │ gzip: 0.51 kB
|
||||
public/build-qa-opportunities/assets/context-documents-Bh7vrWBo.js 1.01 kB │ gzip: 0.57 kB
|
||||
public/build-qa-opportunities/assets/label-Dpfg_IeY.js 1.02 kB │ gzip: 0.62 kB
|
||||
public/build-qa-opportunities/assets/technical-incident-labels-DlL6bSeE.js 1.02 kB │ gzip: 0.51 kB
|
||||
public/build-qa-opportunities/assets/investment-consumer-identity-Cxp_hGfX.js 1.07 kB │ gzip: 0.59 kB
|
||||
public/build-qa-opportunities/assets/article-CPPysFRZ.js 1.08 kB │ gzip: 0.65 kB
|
||||
public/build-qa-opportunities/assets/search-state-RHLy6yI7.js 1.10 kB │ gzip: 0.68 kB
|
||||
public/build-qa-opportunities/assets/rolldown-runtime-Dd_uD5pT.js 1.10 kB │ gzip: 0.62 kB
|
||||
public/build-qa-opportunities/assets/catalogue-investment-map-CTNFBreG.js 1.12 kB │ gzip: 0.62 kB
|
||||
public/build-qa-opportunities/assets/investment-contexts-BQGiwu9n.js 1.17 kB │ gzip: 0.60 kB
|
||||
public/build-qa-opportunities/assets/profile-json-Bza-VQzw.js 1.20 kB │ gzip: 0.72 kB
|
||||
public/build-qa-opportunities/assets/document-LUvEPfWd.js 1.23 kB │ gzip: 0.72 kB
|
||||
public/build-qa-opportunities/assets/shared-JTxA6RNb.js 1.24 kB │ gzip: 0.74 kB
|
||||
public/build-qa-opportunities/assets/public-detail-DLYw1Lrh.js 1.25 kB │ gzip: 0.72 kB
|
||||
public/build-qa-opportunities/assets/demographic-context-BMdtlxjJ.js 1.27 kB │ gzip: 0.73 kB
|
||||
public/build-qa-opportunities/assets/public-offer-card-CudJyoq4.js 1.30 kB │ gzip: 0.70 kB
|
||||
public/build-qa-opportunities/assets/input-BJ-Jg-jz.js 1.37 kB │ gzip: 0.75 kB
|
||||
public/build-qa-opportunities/assets/cluster-competitiveness-hNV0FaaW.js 1.37 kB │ gzip: 0.73 kB
|
||||
public/build-qa-opportunities/assets/verify-email-n6a1zbAv.js 1.40 kB │ gzip: 0.74 kB
|
||||
public/build-qa-opportunities/assets/business-types-SGCXhH6X.js 1.47 kB │ gzip: 0.75 kB
|
||||
public/build-qa-opportunities/assets/verification-BOcn3HIw.js 1.48 kB │ gzip: 0.52 kB
|
||||
public/build-qa-opportunities/assets/country-select-4uP5BvMf.js 1.49 kB │ gzip: 0.78 kB
|
||||
public/build-qa-opportunities/assets/territorial-source-evidence-eKB4ZB36.js 1.53 kB │ gzip: 0.83 kB
|
||||
public/build-qa-opportunities/assets/manage-BH66WeT3.js 1.56 kB │ gzip: 0.74 kB
|
||||
public/build-qa-opportunities/assets/educational-article-card-0rCqZ1kX.js 1.59 kB │ gzip: 0.72 kB
|
||||
public/build-qa-opportunities/assets/partnership-source-links-VfrXk2FZ.js 1.65 kB │ gzip: 0.91 kB
|
||||
public/build-qa-opportunities/assets/catalogue-C8vGjaHy.js 1.78 kB │ gzip: 0.96 kB
|
||||
public/build-qa-opportunities/assets/technical-incident-notice-CjziWA6m.js 1.79 kB │ gzip: 0.89 kB
|
||||
public/build-qa-opportunities/assets/report-DRxtlCFu.js 1.80 kB │ gzip: 0.93 kB
|
||||
public/build-qa-opportunities/assets/preparation-index-CdgPdpQ-.js 1.87 kB │ gzip: 0.89 kB
|
||||
public/build-qa-opportunities/assets/opportunity-technical-incidents-C4aVbut7.js 1.88 kB │ gzip: 0.98 kB
|
||||
public/build-qa-opportunities/assets/password-input-DdsNob7W.js 1.89 kB │ gzip: 1.06 kB
|
||||
public/build-qa-opportunities/assets/passkey-verify-ByR3iZeA.js 2.04 kB │ gzip: 1.15 kB
|
||||
public/build-qa-opportunities/assets/public-record-offline-BmjvnSLE.js 2.07 kB │ gzip: 1.22 kB
|
||||
public/build-qa-opportunities/assets/investors-Di6y94fY.js 2.09 kB │ gzip: 1.15 kB
|
||||
public/build-qa-opportunities/assets/forgot-password-DxizqdUw.js 2.10 kB │ gzip: 0.98 kB
|
||||
public/build-qa-opportunities/assets/sources-BiAS09SV.js 2.13 kB │ gzip: 1.12 kB
|
||||
public/build-qa-opportunities/assets/saved-BppN3tdr.js 2.21 kB │ gzip: 1.10 kB
|
||||
public/build-qa-opportunities/assets/zone-map-XsVlgEXC.js 2.26 kB │ gzip: 1.16 kB
|
||||
public/build-qa-opportunities/assets/appearance-D9-OrC7t.js 2.36 kB │ gzip: 1.14 kB
|
||||
public/build-qa-opportunities/assets/shared-CBJ3c_x-.js 2.37 kB │ gzip: 1.24 kB
|
||||
public/build-qa-opportunities/assets/editorial-subjects-C6_MfRSK.js 2.38 kB │ gzip: 1.14 kB
|
||||
public/build-qa-opportunities/assets/context-installation-BTustRN8.js 2.39 kB │ gzip: 1.23 kB
|
||||
public/build-qa-opportunities/assets/workspace-4_R1CLYI.js 2.40 kB │ gzip: 1.20 kB
|
||||
public/build-qa-opportunities/assets/reset-password-Cn0DRbDq.js 2.43 kB │ gzip: 0.98 kB
|
||||
public/build-qa-opportunities/assets/public-document-content-BHvs9J7S.js 2.47 kB │ gzip: 0.80 kB
|
||||
public/build-qa-opportunities/assets/manage-C2CmrtWp.js 2.47 kB │ gzip: 1.10 kB
|
||||
public/build-qa-opportunities/assets/confirm-password-DkwmRtM5.js 2.47 kB │ gzip: 1.06 kB
|
||||
public/build-qa-opportunities/assets/structures-Dr2nXdub.js 2.49 kB │ gzip: 1.35 kB
|
||||
public/build-qa-opportunities/assets/bootstrap-confirmation-9RmA5-Yz.js 2.55 kB │ gzip: 1.19 kB
|
||||
public/build-qa-opportunities/assets/territory-associations-GTcLYnfG.js 2.55 kB │ gzip: 1.11 kB
|
||||
public/build-qa-opportunities/assets/installation-Dly8Xg6e.js 2.59 kB │ gzip: 1.28 kB
|
||||
public/build-qa-opportunities/assets/technical-support-DlW3Jv7q.js 2.60 kB │ gzip: 1.18 kB
|
||||
public/build-qa-opportunities/assets/management-CvdeRZYq.js 2.68 kB │ gzip: 1.18 kB
|
||||
public/build-qa-opportunities/assets/experiments-BUMjL12A.js 2.73 kB │ gzip: 1.31 kB
|
||||
public/build-qa-opportunities/assets/password-CB_NxgQN.js 2.73 kB │ gzip: 0.73 kB
|
||||
public/build-qa-opportunities/assets/opportunity-install-B232n6lM.js 2.75 kB │ gzip: 1.34 kB
|
||||
public/build-qa-opportunities/assets/dist-BobFCjoG.js 2.92 kB │ gzip: 1.29 kB
|
||||
public/build-qa-opportunities/assets/offer-navigation-DLl-_mMp.js 2.98 kB │ gzip: 1.11 kB
|
||||
public/build-qa-opportunities/assets/portal-api-CpQBRfcu.js 2.98 kB │ gzip: 1.50 kB
|
||||
public/build-qa-opportunities/assets/two-factor-challenge-xfKPtZXs.js 3.24 kB │ gzip: 1.52 kB
|
||||
public/build-qa-opportunities/assets/territory-geometry-preview-4JCbJT47.js 3.25 kB │ gzip: 1.47 kB
|
||||
public/build-qa-opportunities/assets/workspace-C_G1PW62.js 3.29 kB │ gzip: 1.44 kB
|
||||
public/build-qa-opportunities/assets/partnership-agreement-instrument-CCnuZNBy.js 3.43 kB │ gzip: 1.49 kB
|
||||
public/build-qa-opportunities/assets/funding-classification-DheUcd9F.js 3.44 kB │ gzip: 1.64 kB
|
||||
public/build-qa-opportunities/assets/guide-mndEuKFT.js 3.45 kB │ gzip: 1.43 kB
|
||||
public/build-qa-opportunities/assets/history-DSPDM8gM.js 3.46 kB │ gzip: 1.70 kB
|
||||
public/build-qa-opportunities/assets/audit-Xdo8qR4z.js 3.51 kB │ gzip: 1.69 kB
|
||||
public/build-qa-opportunities/assets/show-BJFvXI_v.js 3.60 kB │ gzip: 1.82 kB
|
||||
public/build-qa-opportunities/assets/demography-C6B5IZD_.js 3.70 kB │ gzip: 1.79 kB
|
||||
public/build-qa-opportunities/assets/corridor-agribusiness-BcIVf-hW.js 3.72 kB │ gzip: 1.53 kB
|
||||
public/build-qa-opportunities/assets/business-opportunity-DgZSIWou.js 3.73 kB │ gzip: 1.63 kB
|
||||
public/build-qa-opportunities/assets/management-access-D4VE9LUB.js 3.75 kB │ gzip: 1.43 kB
|
||||
public/build-qa-opportunities/assets/businesses-BSqviy8L.js 3.76 kB │ gzip: 1.57 kB
|
||||
public/build-qa-opportunities/assets/editorial-reading-DUFQmJtD.js 3.89 kB │ gzip: 1.68 kB
|
||||
public/build-qa-opportunities/assets/catalog-B6dpdVsW.js 3.95 kB │ gzip: 1.51 kB
|
||||
public/build-qa-opportunities/assets/desk-assignments-CfWpiIpK.js 3.99 kB │ gzip: 1.95 kB
|
||||
public/build-qa-opportunities/assets/dashboard-CxoTLkSg.js 3.99 kB │ gzip: 1.61 kB
|
||||
public/build-qa-opportunities/assets/investment-context-cards-gJe_oyxw.js 4.05 kB │ gzip: 1.84 kB
|
||||
public/build-qa-opportunities/assets/shared-OMougC44.js 4.09 kB │ gzip: 2.05 kB
|
||||
public/build-qa-opportunities/assets/opportunity-contact-form-Cb6iDQ1U.js 4.10 kB │ gzip: 1.67 kB
|
||||
public/build-qa-opportunities/assets/demographic-evidence-DGwOnf5g.js 4.38 kB │ gzip: 1.84 kB
|
||||
public/build-qa-opportunities/assets/conversation-DmGmFHQ_.js 4.42 kB │ gzip: 1.95 kB
|
||||
public/build-qa-opportunities/assets/portfolio-BCzBidHt.js 4.45 kB │ gzip: 1.74 kB
|
||||
public/build-qa-opportunities/assets/reference-CC7DXgRa.js 4.46 kB │ gzip: 2.00 kB
|
||||
public/build-qa-opportunities/assets/territory-geometry-import-BnSl5jzG.js 4.49 kB │ gzip: 1.90 kB
|
||||
public/build-qa-opportunities/assets/opportunity-origin-D3o6OTRg.js 4.64 kB │ gzip: 2.07 kB
|
||||
public/build-qa-opportunities/assets/context-inheritance-dd0VgP8h.js 4.64 kB │ gzip: 2.00 kB
|
||||
public/build-qa-opportunities/assets/installation-CnslW5oi.js 4.67 kB │ gzip: 1.95 kB
|
||||
public/build-qa-opportunities/assets/opportunities-YsMGMCR_.js 4.73 kB │ gzip: 1.77 kB
|
||||
public/build-qa-opportunities/assets/references-BMPGGgyS.js 4.79 kB │ gzip: 2.22 kB
|
||||
public/build-qa-opportunities/assets/offer-confidential-access-Bu9EgZ3d.js 4.83 kB │ gzip: 1.91 kB
|
||||
public/build-qa-opportunities/assets/opportunity-curation-7Gt2cWer.js 4.88 kB │ gzip: 2.04 kB
|
||||
public/build-qa-opportunities/assets/business-verifications-CI0i9uoo.js 4.90 kB │ gzip: 1.71 kB
|
||||
public/build-qa-opportunities/assets/crm-edit-comparison-Db53T7I7.js 4.91 kB │ gzip: 1.99 kB
|
||||
public/build-qa-opportunities/assets/investment-complements-BxgtKBx2.js 5.00 kB │ gzip: 2.05 kB
|
||||
public/build-qa-opportunities/assets/service-CbUB6_Ll.js 5.05 kB │ gzip: 2.33 kB
|
||||
public/build-qa-opportunities/assets/agreement-history-BfSa8E7Q.js 5.13 kB │ gzip: 2.10 kB
|
||||
public/build-qa-opportunities/assets/catalogue-Cd-RNIG1.js 5.13 kB │ gzip: 2.54 kB
|
||||
public/build-qa-opportunities/assets/workspace-BeyUwWtD.js 5.20 kB │ gzip: 2.18 kB
|
||||
public/build-qa-opportunities/assets/article-editor-ByLxbPbJ.js 5.25 kB │ gzip: 2.20 kB
|
||||
public/build-qa-opportunities/assets/context-start-BcJpYa9I.js 5.46 kB │ gzip: 2.09 kB
|
||||
public/build-qa-opportunities/assets/economic-references-C7dNVUC6.js 5.61 kB │ gzip: 2.52 kB
|
||||
public/build-qa-opportunities/assets/convert-B3LtI2n6.js 5.81 kB │ gzip: 2.40 kB
|
||||
public/build-qa-opportunities/assets/editorial-subject-form-B2W8E2NJ.js 5.87 kB │ gzip: 2.66 kB
|
||||
public/build-qa-opportunities/assets/public-sector-cards-Esb1aTm6.js 5.94 kB │ gzip: 2.55 kB
|
||||
public/build-qa-opportunities/assets/demography-install-B57rikBv.js 6.05 kB │ gzip: 2.50 kB
|
||||
public/build-qa-opportunities/assets/context-DX8H9xrC.js 6.06 kB │ gzip: 2.65 kB
|
||||
public/build-qa-opportunities/assets/request-CEHLhPPo.js 6.10 kB │ gzip: 2.41 kB
|
||||
public/build-qa-opportunities/assets/structure-geography-Bo85NWTp.js 6.12 kB │ gzip: 2.22 kB
|
||||
public/build-qa-opportunities/assets/editorial-workflow-DYYZzXOw.js 6.22 kB │ gzip: 2.29 kB
|
||||
public/build-qa-opportunities/assets/investment-map-panel-B1xwyvFR.js 6.27 kB │ gzip: 2.88 kB
|
||||
public/build-qa-opportunities/assets/procurement-PtO-r9qV.js 6.35 kB │ gzip: 2.73 kB
|
||||
public/build-qa-opportunities/assets/register-DyaxliT7.js 6.43 kB │ gzip: 2.71 kB
|
||||
public/build-qa-opportunities/assets/funding-BslGAfF5.js 6.49 kB │ gzip: 2.00 kB
|
||||
public/build-qa-opportunities/assets/leads-Bu2UK8ON.js 6.56 kB │ gzip: 2.82 kB
|
||||
public/build-qa-opportunities/assets/business-marketplace-DGH7B-E2.js 6.59 kB │ gzip: 2.51 kB
|
||||
public/build-qa-opportunities/assets/interest-xqtiIFPC.js 6.63 kB │ gzip: 2.23 kB
|
||||
public/build-qa-opportunities/assets/demography-BxaBdOBx.js 6.66 kB │ gzip: 2.51 kB
|
||||
public/build-qa-opportunities/assets/fiscal-qa-review-CEzw2OQP.js 6.75 kB │ gzip: 2.70 kB
|
||||
public/build-qa-opportunities/assets/business-interest-CZgFv7wk.js 6.82 kB │ gzip: 2.60 kB
|
||||
public/build-qa-opportunities/assets/interests-BcpPowrJ.js 6.83 kB │ gzip: 2.78 kB
|
||||
public/build-qa-opportunities/assets/catalogue-GV7YGM69.js 6.84 kB │ gzip: 2.81 kB
|
||||
public/build-qa-opportunities/assets/company-comparison-CLzhILGo.js 6.92 kB │ gzip: 2.60 kB
|
||||
public/build-qa-opportunities/assets/business-classification-CbEGwmnS.js 7.01 kB │ gzip: 2.57 kB
|
||||
public/build-qa-opportunities/assets/dist-Ddn_hEUd.js 7.02 kB │ gzip: 2.71 kB
|
||||
public/build-qa-opportunities/assets/blank-2jFZEe2q.js 7.07 kB │ gzip: 2.77 kB
|
||||
public/build-qa-opportunities/assets/analysis-CWzzPBct.js 7.11 kB │ gzip: 2.96 kB
|
||||
public/build-qa-opportunities/assets/profile-7Z3ih2bC.js 7.19 kB │ gzip: 2.47 kB
|
||||
public/build-qa-opportunities/assets/install-ChSwGq-d.js 7.23 kB │ gzip: 3.03 kB
|
||||
public/build-qa-opportunities/assets/investor-COYuuRY_.js 7.30 kB │ gzip: 2.89 kB
|
||||
public/build-qa-opportunities/assets/investor-path-CSExMjAt.js 7.36 kB │ gzip: 3.02 kB
|
||||
public/build-qa-opportunities/assets/proactive-supply-review-Bspf5LP7.js 7.55 kB │ gzip: 2.73 kB
|
||||
public/build-qa-opportunities/assets/legacy-Cgz97_0u.js 7.67 kB │ gzip: 2.56 kB
|
||||
public/build-qa-opportunities/assets/jsx-runtime-BpzPEenQ.js 7.89 kB │ gzip: 2.99 kB
|
||||
public/build-qa-opportunities/assets/shared-BFCbvJo7.js 7.90 kB │ gzip: 3.08 kB
|
||||
public/build-qa-opportunities/assets/business-objects-BSbz_1pe.js 7.95 kB │ gzip: 2.57 kB
|
||||
public/build-qa-opportunities/assets/assisted-K2_qpkSv.js 8.06 kB │ gzip: 3.16 kB
|
||||
public/build-qa-opportunities/assets/value-network-Cp8UYtMs.js 8.06 kB │ gzip: 3.08 kB
|
||||
public/build-qa-opportunities/assets/editor-xaCzg6Ao.js 8.21 kB │ gzip: 2.97 kB
|
||||
public/build-qa-opportunities/assets/territorial-editorial-B0ni24WA.js 8.22 kB │ gzip: 3.03 kB
|
||||
public/build-qa-opportunities/assets/opportunity-promotion-CyWpImc1.js 8.40 kB │ gzip: 2.96 kB
|
||||
public/build-qa-opportunities/assets/publication-history-DfLO6jOc.js 8.42 kB │ gzip: 3.02 kB
|
||||
public/build-qa-opportunities/assets/print-Tn3aXHlY.js 8.66 kB │ gzip: 3.55 kB
|
||||
public/build-qa-opportunities/assets/dialog-D0NJ4SGy.js 8.81 kB │ gzip: 2.76 kB
|
||||
public/build-qa-opportunities/assets/shared-DXIXdF9G.js 8.92 kB │ gzip: 3.83 kB
|
||||
public/build-qa-opportunities/assets/iva-modelo-a-CNylSdBc.js 8.94 kB │ gzip: 3.27 kB
|
||||
public/build-qa-opportunities/assets/zone-relations-CdbqX19L.js 8.95 kB │ gzip: 2.99 kB
|
||||
public/build-qa-opportunities/assets/investment-territory-CTzFQmCV.js 8.96 kB │ gzip: 3.43 kB
|
||||
public/build-qa-opportunities/assets/similar-businesses-HTwQGVGQ.js 9.11 kB │ gzip: 3.58 kB
|
||||
public/build-qa-opportunities/assets/interactive-investment-map-CGOhHCge.js 9.13 kB │ gzip: 3.60 kB
|
||||
public/build-qa-opportunities/assets/investment-land-preparation-BHeC9B2U.js 9.15 kB │ gzip: 3.33 kB
|
||||
public/build-qa-opportunities/assets/why-invest-BIS-cmeJ.js 9.16 kB │ gzip: 3.49 kB
|
||||
public/build-qa-opportunities/assets/interest-history-oHNykKmC.js 9.48 kB │ gzip: 3.45 kB
|
||||
public/build-qa-opportunities/assets/content-hub-Df5xDkkT.js 9.95 kB │ gzip: 2.93 kB
|
||||
public/build-qa-opportunities/assets/privacy-policy-LI1lpUCK.js 10.36 kB │ gzip: 3.07 kB
|
||||
public/build-qa-opportunities/assets/editorial-edition-Zq39X7mf.js 10.36 kB │ gzip: 3.47 kB
|
||||
public/build-qa-opportunities/assets/panel-Bi6FJvJx.js 10.46 kB │ gzip: 4.29 kB
|
||||
public/build-qa-opportunities/assets/result-yez5wOFo.js 10.57 kB │ gzip: 4.73 kB
|
||||
public/build-qa-opportunities/assets/dashboard-mFja_TO4.js 10.92 kB │ gzip: 3.47 kB
|
||||
public/build-qa-opportunities/assets/zone-attachments-DBvjiTcG.js 10.94 kB │ gzip: 2.75 kB
|
||||
public/build-qa-opportunities/assets/business-DUxcRYkb.js 11.10 kB │ gzip: 3.08 kB
|
||||
public/build-qa-opportunities/assets/detail-BRdMNEQp.js 11.73 kB │ gzip: 4.31 kB
|
||||
public/build-qa-opportunities/assets/login-CDV8vvsd.js 11.89 kB │ gzip: 4.92 kB
|
||||
public/build-qa-opportunities/assets/support-installation-tsBpnQkP.js 12.36 kB │ gzip: 4.86 kB
|
||||
public/build-qa-opportunities/assets/show-DF9KhUzw.js 12.37 kB │ gzip: 3.98 kB
|
||||
public/build-qa-opportunities/assets/tempo-brand-BXcnRsyn.js 12.38 kB │ gzip: 5.01 kB
|
||||
public/build-qa-opportunities/assets/show-CAVO7psn.js 12.40 kB │ gzip: 4.39 kB
|
||||
public/build-qa-opportunities/assets/guide-DcEhT0Sh.js 12.58 kB │ gzip: 4.03 kB
|
||||
public/build-qa-opportunities/assets/contact-BwgG52Cs.js 12.65 kB │ gzip: 4.33 kB
|
||||
public/build-qa-opportunities/assets/project-dossier-document-DLCZGheg.js 12.68 kB │ gzip: 5.20 kB
|
||||
public/build-qa-opportunities/assets/dossier-prefill-B18BbJoD.js 12.72 kB │ gzip: 4.64 kB
|
||||
public/build-qa-opportunities/assets/technical-incident-GDB_x0wp.js 12.73 kB │ gzip: 4.96 kB
|
||||
public/build-qa-opportunities/assets/competitiveness-60nqrOfg.js 12.74 kB │ gzip: 4.23 kB
|
||||
public/build-qa-opportunities/assets/editor-DMwEWxsJ.js 12.91 kB │ gzip: 3.75 kB
|
||||
public/build-qa-opportunities/assets/fiscal-forms-CIgeIrrS.js 13.04 kB │ gzip: 4.75 kB
|
||||
public/build-qa-opportunities/assets/opportunity-classification-Dm_W66ep.js 13.44 kB │ gzip: 4.55 kB
|
||||
public/build-qa-opportunities/assets/react-CjVep8xp.js 13.90 kB │ gzip: 4.61 kB
|
||||
public/build-qa-opportunities/assets/opportunity-search-filters-CqGDv55D.js 14.74 kB │ gzip: 4.72 kB
|
||||
public/build-qa-opportunities/assets/case-wwTeoOmv.js 14.77 kB │ gzip: 5.38 kB
|
||||
public/build-qa-opportunities/assets/product-usage-A6hb-0oX.js 14.81 kB │ gzip: 5.29 kB
|
||||
public/build-qa-opportunities/assets/company-preparation-D8eVac1u.js 15.17 kB │ gzip: 4.92 kB
|
||||
public/build-qa-opportunities/assets/terms-of-use-CUQdwKes.js 15.27 kB │ gzip: 4.20 kB
|
||||
public/build-qa-opportunities/assets/edit-DsH5Q1h1.js 15.38 kB │ gzip: 5.15 kB
|
||||
public/build-qa-opportunities/assets/relationship-repair-brZoqSge.js 16.29 kB │ gzip: 5.62 kB
|
||||
public/build-qa-opportunities/assets/edit-BPe9IHkr.js 16.60 kB │ gzip: 4.88 kB
|
||||
public/build-qa-opportunities/assets/edit-CheIw-hI.js 16.93 kB │ gzip: 4.91 kB
|
||||
public/build-qa-opportunities/assets/search-B8kmEkH5.js 17.16 kB │ gzip: 5.69 kB
|
||||
public/build-qa-opportunities/assets/funding-institution-DgwDRv_z.js 17.24 kB │ gzip: 5.82 kB
|
||||
public/build-qa-opportunities/assets/use-two-factor-auth-D9w0KtSq.js 17.31 kB │ gzip: 6.30 kB
|
||||
public/build-qa-opportunities/assets/administration-CgUGyaX7.js 17.41 kB │ gzip: 4.30 kB
|
||||
public/build-qa-opportunities/assets/notifications-Tmebv7ru.js 17.61 kB │ gzip: 5.83 kB
|
||||
public/build-qa-opportunities/assets/ai-ykPV8ntL.js 18.08 kB │ gzip: 6.92 kB
|
||||
public/build-qa-opportunities/assets/project-dossier-report-DpD9_xNw.js 18.98 kB │ gzip: 6.95 kB
|
||||
public/build-qa-opportunities/assets/submission-C6KF6kEv.js 19.99 kB │ gzip: 6.32 kB
|
||||
public/build-qa-opportunities/assets/opportunity-resources-DHgylXUE.js 20.20 kB │ gzip: 7.22 kB
|
||||
public/build-qa-opportunities/assets/about-BtlawfeW.js 20.95 kB │ gzip: 7.03 kB
|
||||
public/build-qa-opportunities/assets/zones-Cqg6mKe2.js 21.16 kB │ gzip: 5.22 kB
|
||||
public/build-qa-opportunities/assets/service-editor-CG1DhyKz.js 21.30 kB │ gzip: 6.58 kB
|
||||
public/build-qa-opportunities/assets/context-DBE0uEqB.js 21.56 kB │ gzip: 6.99 kB
|
||||
public/build-qa-opportunities/assets/declaration-Bzl3lixx.js 22.15 kB │ gzip: 7.14 kB
|
||||
public/build-qa-opportunities/assets/Combination-BBDI-RbM.js 23.03 kB │ gzip: 8.24 kB
|
||||
public/build-qa-opportunities/assets/desk-internal-operations-BakRT6ga.js 23.14 kB │ gzip: 6.53 kB
|
||||
public/build-qa-opportunities/assets/education-BElOGGwI.js 23.40 kB │ gzip: 8.20 kB
|
||||
public/build-qa-opportunities/assets/investment-opportunity-choices-BIDCuOIq.js 24.28 kB │ gzip: 8.06 kB
|
||||
public/build-qa-opportunities/assets/preparation-D_D73Qym.js 24.56 kB │ gzip: 6.73 kB
|
||||
public/build-qa-opportunities/assets/utils-CCDJ05Ll.js 27.31 kB │ gzip: 8.71 kB
|
||||
public/build-qa-opportunities/assets/business-wizard-7qn-T-8z.js 28.60 kB │ gzip: 9.42 kB
|
||||
public/build-qa-opportunities/assets/proactive-investment-DfIQuXvs.js 28.65 kB │ gzip: 9.16 kB
|
||||
public/build-qa-opportunities/assets/lead-vUXgd2g8.js 29.03 kB │ gzip: 9.96 kB
|
||||
public/build-qa-opportunities/assets/preparation-BJOKOlYB.js 29.25 kB │ gzip: 8.51 kB
|
||||
public/build-qa-opportunities/assets/form-CuZokTHN.js 29.80 kB │ gzip: 8.04 kB
|
||||
public/build-qa-opportunities/assets/profile-DSwkSvJs.js 30.71 kB │ gzip: 9.33 kB
|
||||
public/build-qa-opportunities/assets/welcome-_9nm1hgc.js 30.73 kB │ gzip: 6.49 kB
|
||||
public/build-qa-opportunities/assets/manage-GV_WvYCX.js 31.17 kB │ gzip: 7.63 kB
|
||||
public/build-qa-opportunities/assets/security-CB7TJeqg.js 32.11 kB │ gzip: 10.06 kB
|
||||
public/build-qa-opportunities/assets/editor-CWbYf7YK.js 35.03 kB │ gzip: 11.76 kB
|
||||
public/build-qa-opportunities/assets/case-CPWSbxvW.js 37.38 kB │ gzip: 10.32 kB
|
||||
public/build-qa-opportunities/assets/room-Bg8k9V54.js 38.25 kB │ gzip: 12.14 kB
|
||||
public/build-qa-opportunities/assets/simulation-form-CVV000gF.js 39.43 kB │ gzip: 10.27 kB
|
||||
public/build-qa-opportunities/assets/request-Duh2vDwb.js 41.86 kB │ gzip: 11.30 kB
|
||||
public/build-qa-opportunities/assets/detail-CXufoj8g.js 46.42 kB │ gzip: 15.18 kB
|
||||
public/build-qa-opportunities/assets/PortalInvestidor-DCM4Umjc.js 48.72 kB │ gzip: 14.86 kB
|
||||
public/build-qa-opportunities/assets/business-financial-plan-s1uoiktr.js 49.19 kB │ gzip: 11.98 kB
|
||||
public/build-qa-opportunities/assets/catalogue-SjEs3uS8.js 50.02 kB │ gzip: 16.10 kB
|
||||
public/build-qa-opportunities/assets/dropdown-menu-DeBy_kF_.js 57.05 kB │ gzip: 19.02 kB
|
||||
public/build-qa-opportunities/assets/zone-B2czy9Xo.js 62.42 kB │ gzip: 15.63 kB
|
||||
public/build-qa-opportunities/assets/show-9fSIEkpb.js 68.49 kB │ gzip: 21.16 kB
|
||||
public/build-qa-opportunities/assets/opportunity-draft-Cei20Tpo.js 70.65 kB │ gzip: 20.89 kB
|
||||
public/build-qa-opportunities/assets/document-zip-bqFSTgG4.js 95.97 kB │ gzip: 28.50 kB
|
||||
public/build-qa-opportunities/assets/app-7Rc_PO3G.js 147.54 kB │ gzip: 45.60 kB
|
||||
public/build-qa-opportunities/assets/leaflet-src-CUWrUTR2.js 148.82 kB │ gzip: 43.39 kB
|
||||
public/build-qa-opportunities/assets/dist-BRfsww6z.js 307.99 kB │ gzip: 96.67 kB
|
||||
public/build-qa-opportunities/assets/document-word-CqTGSyXu.js 403.66 kB │ gzip: 115.44 kB
|
||||
public/build-qa-opportunities/assets/services-vnbZkLSz.js 965.96 kB │ gzip: 226.50 kB
|
||||
public/build-qa-opportunities/assets/project-dossier-a3IvmE5B.js 1,009.94 kB │ gzip: 274.44 kB
|
||||
|
||||
[plugin builtin:vite-reporter]
|
||||
(!) Some chunks are larger than 500 kB after minification. Consider:
|
||||
- Using dynamic import() to code-split the application
|
||||
- Use build.rolldownOptions.output.codeSplitting to improve chunking:
|
||||
https://rolldown.rs/reference/OutputOptions.codeSplitting
|
||||
- Adjust chunk size limit for this warning via build.chunkSizeWarningLimit.
|
||||
[33m[PLUGIN_TIMINGS] [0mYour build spent 98% of 24.5s inside plugin hooks (24.0s).
|
||||
Measured inside the callback, so queue time is excluded and time the callback itself awaited is not:
|
||||
- @rolldown/plugin-babel transform (78%, 19.1s, 412 calls)
|
||||
- @tailwindcss/vite:generate:build transform (16%, 3.9s, 3 calls)
|
||||
Those rows are 94% of the build; the rest of the 98% is below.
|
||||
Not measurable — 1 hook whose calls overlap, so elapsed time covers work other calls were doing. Profile with `node
|
||||
--cpu-prof`:
|
||||
- vite:css transform (3 calls)
|
||||
See https://rolldown.rs/reference/InputOptions.checks#plugintimings for more details.
|
||||
|
||||
✓ built in 24.52s
|
||||
@@ -0,0 +1,37 @@
|
||||
✔ each manager dashboard menu anchor resolves to a real section (11.504167ms)
|
||||
✔ investor section remains distinct from administrative summary (0.786958ms)
|
||||
✔ sidebar renders every nested authorised destination instead of a first-child shortcut (32.997667ms)
|
||||
✔ nested current destination is selected without changing its URL or replacing it with a group root (8.242541ms)
|
||||
✔ rendered visitor sidebar explains access without private operation links (8.582708ms)
|
||||
✔ sidebar carries the existing search territory when moving between catalogues (11.782958ms)
|
||||
✔ changing offer state preserves the operational and pending-report filters (29.420375ms)
|
||||
✔ private offers use the existing work group and do not appear for an ungranted account (6.359416ms)
|
||||
✔ public and authenticated navigation share groups without revealing management (6.903875ms)
|
||||
✔ management and optional modules follow permissions and feature flags (1.518125ms)
|
||||
✔ detail URLs and filtered catalogue pages retain the current group (2.9345ms)
|
||||
✔ reconciled navigation selects discovery, realization and private support correctly (0.506791ms)
|
||||
✔ discovery retains distinct catalogues without repeating main entry points (0.444541ms)
|
||||
✔ catalogue detail and return preserve the search inside the local catalogue (0.951917ms)
|
||||
✔ both menu consumers respect the existing scoped server decision rather than generic manager status (0.457583ms)
|
||||
✔ work entry is discoverable without exposing private capabilities to an anonymous visitor (0.628042ms)
|
||||
✔ work area exposes existing content capability even when the old dashboard flag has no menu object (2.436209ms)
|
||||
✔ every previously authorised management/profile destination survives the regrouping exactly once (0.993917ms)
|
||||
✔ ordinary authenticated account has an explanation entry and does not acquire creation or management links (0.281292ms)
|
||||
✔ direct analysis and ordinary creation preserve distinct selection state in the same work group (0.657375ms)
|
||||
✔ investment entry belongs to realization exactly once (0.336625ms)
|
||||
✔ portfolio and zone fiche return keeps the applied catalogue page and filters (0.665541ms)
|
||||
✔ catalogue pagination inheritance rejects invalid page values (0.243834ms)
|
||||
✔ funding links require installation and saved list requires authentication (0.728958ms)
|
||||
✔ installed concours has its own primary group and preserves public and private destinations (0.63125ms)
|
||||
✔ administration owns dashboard activities once and remains hidden without authorised destinations (0.932792ms)
|
||||
✔ administrative operations are grouped outside Dashboard and removed from More/work (0.444ms)
|
||||
✔ actual header has exactly the requested six primary entries on desktop and mobile for public and signed-in users (41.499292ms)
|
||||
✔ authorised administration is primary and dashboard is a nested expandable group on desktop and mobile (10.578291ms)
|
||||
ℹ tests 29
|
||||
ℹ suites 0
|
||||
ℹ pass 29
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 1226.643584
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"scope": "Offer navigation only; existing horizontal portal dropdowns reused",
|
||||
"frontend_tests": {
|
||||
"tests": 29,
|
||||
"passed": 29
|
||||
},
|
||||
"types": "passed",
|
||||
"build": "QA build passed; assets served from existing local preview",
|
||||
"lint": "passed with one pre-existing prefer-string-starts-ends-with warning outside changed lines in portal-navigation.ts",
|
||||
"backend": "unchanged; no database-refreshing tests run during user preview",
|
||||
"http": [
|
||||
{
|
||||
"path": "/minhas-oportunidades/ofertas",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"path": "/minhas-oportunidades/ofertas/18",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"path": "/minhas-oportunidades/ofertas/19",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"asset": "assets/app-7Rc_PO3G.js",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"asset": "assets/preparation-BJOKOlYB.js",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"asset": "assets/preparation-index-CdgPdpQ-.js",
|
||||
"status": 200
|
||||
}
|
||||
],
|
||||
"data_and_session": "Existing synthetic identities and offers preserved; no seeding, migration, reset or session invalidation during this change",
|
||||
"publication": "unchanged, closed",
|
||||
"installed_in_homologation": false,
|
||||
"visual_acceptance": "pending user review; no browser screenshot acceptance claimed",
|
||||
"previous_bundle": "9a014268 bundle preserved; this UI delta is not included in that previously frozen package",
|
||||
"source_sha256": {
|
||||
"resources/js/app.tsx": "b6afda8c8d60e8049d53352a6b62b7cabf0a470adc31a55c952c6e71456986a1",
|
||||
"resources/js/lib/portal-navigation.ts": "4fb7cbf0a508cf0964e7ea0dd2023bc00202236bf048f228822d7de6e1f44534",
|
||||
"resources/js/lib/offer-navigation.ts": "94ec55b7bbbc800f57fd10a9b51a49398c6cdd23fa0df635ca20e949426aef13",
|
||||
"resources/js/components/offer-navigation.tsx": "4676f4859f7d378ebca988bc5436629f97c0004abe32af6db8a6b40789b48874",
|
||||
"resources/js/pages/offers/preparation.tsx": "7503946ba28e71ff48e6fd955b1ed587494678df433b6a13492dd026b74ce1de",
|
||||
"resources/js/pages/offers/preparation-index.tsx": "1cdfe52e4ed8311f52e24caab32d83dc7611213e5b9d3cc477b61c6e6c996276",
|
||||
"tests/frontend/offer-navigation.test.mjs": "007122b87d4123a6dfd5360b106821a3c27e6ba1575bae4d77ed631da7281ed9"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
|
||||
> types:check
|
||||
> tsc --noEmit
|
||||
@@ -0,0 +1,68 @@
|
||||
set -eu
|
||||
python3 - <<'PY'
|
||||
from pathlib import Path
|
||||
import os,subprocess,json,hashlib,tarfile,time
|
||||
assert os.geteuid()==0 and os.environ.get('SSH_CONNECTION')
|
||||
deadline=time.monotonic()+90
|
||||
while subprocess.run(['systemctl','is-active','teste-investidor-deploy.service'],capture_output=True,text=True).stdout.strip() not in ['inactive','failed']:
|
||||
if time.monotonic()>deadline: raise RuntimeError('Existing deployment still active; no backup or schema change performed')
|
||||
time.sleep(3)
|
||||
assert json.loads(Path('/var/lib/investmoz-deploy/installed.json').read_text())['commit']=='96f510965f70581a4b41dd94463f598b77dd7cf5', 'Destination advanced; reconcile first'
|
||||
assert subprocess.run(['systemctl','is-active','teste-investidor-deploy.service'],capture_output=True,text=True).stdout.strip() in ['inactive','failed']
|
||||
root=Path('/var/www/html/teste-investidor.tempo.co.mz/current').resolve()
|
||||
stage=Path('/var/backups/investmoz-offers-preinstall-20261114/backup')
|
||||
assert not stage.exists(), 'Existing backup must be reconciled before repeating'
|
||||
assert __import__('shutil').disk_usage(root).free >= 5*1024**3, 'Capacity changed; active release preserved'
|
||||
stage.mkdir(mode=0o700,parents=True)
|
||||
php=r'''<?php
|
||||
require 'vendor/autoload.php';$app=require 'bootstrap/app.php';$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();
|
||||
use Illuminate\Support\Facades\DB;
|
||||
DB::beginTransaction(); DB::statement('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY'); DB::statement("SET TIME ZONE 'UTC'");
|
||||
try {
|
||||
$identity=DB::selectOne('select current_database() as database,inet_server_port() as port');
|
||||
if($identity->database!=='teste_investidor'||(int)$identity->port!==5432)throw new RuntimeException('Wrong active source');
|
||||
$snapshot=DB::selectOne('select pg_export_snapshot() as id')->id;
|
||||
$tables=[];$sql="BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY; SET TRANSACTION SNAPSHOT '".$snapshot."'; SET TIME ZONE 'UTC';\n";
|
||||
foreach(DB::select("select schemaname,tablename from pg_tables where schemaname not in ('pg_catalog','information_schema') and schemaname not like 'pg_%' order by schemaname,tablename") as $t){
|
||||
$name=$t->schemaname.'.'.$t->tablename;$quoted='"'.str_replace('"','""',$t->schemaname).'"."'.str_replace('"','""',$t->tablename).'"';
|
||||
$sql.="select json_build_object('name','".$name."','rows',count(*),'checksum',md5(coalesce(string_agg(row_to_json(t)::text,E'\\n' order by md5(row_to_json(t)::text) COLLATE \"C\"),''))) from ".$quoted." t;\n";
|
||||
}
|
||||
$sql.='ROLLBACK;';$reader=new Symfony\Component\Process\Process(['runuser','-u','postgres','--','psql','-X','-q','-A','-t','-v','ON_ERROR_STOP=1','-d','teste_investidor']);$reader->setInput($sql)->setTimeout(90)->mustRun();
|
||||
foreach(explode("\n",trim($reader->getOutput())) as $line){$row=json_decode($line,true,512,JSON_THROW_ON_ERROR);$name=$row['name'];unset($row['name']);$tables[$name]=$row;}
|
||||
$process=new Symfony\Component\Process\Process(['runuser','-u','postgres','--','pg_dump','--format=custom','--no-owner','--no-privileges','--lock-wait-timeout=10000','--snapshot='.$snapshot,'teste_investidor']);
|
||||
$process->setTimeout(90)->mustRun();file_put_contents('/var/backups/investmoz-offers-preinstall-20261114/backup/database.dump',$process->getOutput());
|
||||
echo json_encode(['target'=>$identity,'tables'=>$tables,'consistent_exported_snapshot'=>true,'importer_fingerprint'=>app(App\Services\TerritoryGeometryImport::class)->fingerprint(),'scanner'=>['socket'=>config('services.malware.socket'),'host'=>config('services.malware.host'),'port'=>config('services.malware.port')]],JSON_THROW_ON_ERROR);
|
||||
}finally{DB::rollBack();}
|
||||
'''
|
||||
answer=json.loads(subprocess.check_output(['php8.4'],input=php.encode(),cwd=root))
|
||||
dump=stage/'database.dump';os.chmod(dump,0o600)
|
||||
listing=subprocess.check_output(['pg_restore','--list',str(dump)]).decode()
|
||||
assert 'TABLE DATA public geo_locations' in listing and 'TABLE DATA public geo_geometries' in listing
|
||||
storage=(root/'storage/app').resolve()
|
||||
manifest={}
|
||||
for p in sorted(storage.rglob('*')):
|
||||
if p.is_file() and not p.is_symlink():manifest['storage/app/'+str(p.relative_to(storage))]={'bytes':p.stat().st_size,'sha256':hashlib.sha256(p.read_bytes()).hexdigest()}
|
||||
env=(root/'.env').resolve();manifest['configuration/.env']={'bytes':env.stat().st_size,'sha256':hashlib.sha256(env.read_bytes()).hexdigest()}
|
||||
archive=stage/'files-before.tar.gz'
|
||||
with tarfile.open(archive,'w:gz') as tf:
|
||||
tf.add(storage,arcname='storage/app',recursive=True);tf.add(env,arcname='configuration/.env',recursive=False)
|
||||
os.chmod(archive,0o600)
|
||||
verified=0
|
||||
with tarfile.open(archive,'r:gz') as tf:
|
||||
for member in tf:
|
||||
if member.isfile():
|
||||
expected=manifest[member.name];f=tf.extractfile(member);digest=hashlib.sha256(f.read()).hexdigest()
|
||||
assert digest==expected['sha256'] and member.size==expected['bytes'];verified+=1
|
||||
assert verified==len(manifest)
|
||||
answer.update({'created_at':time.strftime('%Y-%m-%dT%H:%M:%SZ',time.gmtime()),'dump_path':str(dump),'sha256':hashlib.sha256(dump.read_bytes()).hexdigest(),'files_path':str(archive),'files_sha256':hashlib.sha256(archive.read_bytes()).hexdigest(),'files_verified':True,'file_count':verified,'file_manifest':manifest,'active_data_writes':0,'restore_verified':False,'root':str(root)})
|
||||
receipt=stage/'backup-receipt.json';receipt.write_text(json.dumps(answer,indent=2));os.chmod(receipt,0o600)
|
||||
assert json.loads(Path('/var/lib/investmoz-deploy/installed.json').read_text())['commit']=='96f510965f70581a4b41dd94463f598b77dd7cf5', 'Source revision changed during backup; do not install'
|
||||
print(json.dumps({'stage':str(stage),'created_at':answer['created_at'],'target':answer['target'],'tables':len(answer['tables']),'files_verified':verified,'dump_sha256':answer['sha256'],'files_sha256':answer['files_sha256'],'restore_verified':False}))
|
||||
|
||||
import shutil
|
||||
for name in ['delivery.json','schema-contract.json','ci-environment.json']:
|
||||
shutil.copy2('/etc/investmoz-deploy/'+name,stage/name)
|
||||
shutil.copy2('/var/lib/investmoz-deploy/installed.json',stage/'installed.json')
|
||||
shutil.copy2('/var/www/deploy/investmoz-pipeline.py',stage/'pipeline.py')
|
||||
|
||||
PY
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"stage": "/var/backups/investmoz-offers-preinstall-20261114/backup",
|
||||
"created_at": "2026-11-14T17:34:37Z",
|
||||
"target": {
|
||||
"database": "teste_investidor",
|
||||
"port": 5432
|
||||
},
|
||||
"tables": 219,
|
||||
"files_verified": 417,
|
||||
"dump_sha256": "662c5d9f1fe7e01a5ccf7529c8c8e8f27205907d60d2da75a580b7b6c37c252e",
|
||||
"files_sha256": "f438f31f212e516442526b609bf5ac02cfd2f630a6ac2a2b6fc12f1f88717a42",
|
||||
"restore_verified": false
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"environment": "local",
|
||||
"postgres_version": "17.11 (Debian 17.11-1.pgdg12+2)",
|
||||
"extensions": [
|
||||
{
|
||||
"extname": "postgis",
|
||||
"extversion": "3.6.4"
|
||||
},
|
||||
{
|
||||
"extname": "pgcrypto",
|
||||
"extversion": "1.3"
|
||||
}
|
||||
],
|
||||
"notification_constraints": [
|
||||
{
|
||||
"conname": "editorial_native_notice_unique"
|
||||
},
|
||||
{
|
||||
"conname": "portal_editorial_notice_subject"
|
||||
},
|
||||
{
|
||||
"conname": "portal_editorial_notifications_edition_id_foreign"
|
||||
},
|
||||
{
|
||||
"conname": "portal_editorial_notifications_native_event_id_foreign"
|
||||
},
|
||||
{
|
||||
"conname": "portal_editorial_notifications_opportunity_id_foreign"
|
||||
},
|
||||
{
|
||||
"conname": "portal_editorial_notifications_pkey"
|
||||
},
|
||||
{
|
||||
"conname": "portal_editorial_notifications_user_id_foreign"
|
||||
},
|
||||
{
|
||||
"conname": "portal_editorial_op_notice_uq"
|
||||
}
|
||||
],
|
||||
"notification_unique_index_present": true,
|
||||
"offer_code_present": {
|
||||
"preparation": false,
|
||||
"operations": false,
|
||||
"public_catalogue": false
|
||||
},
|
||||
"offer_notification_jobs": 0,
|
||||
"offer_notification_failures": 0,
|
||||
"operational_writes": 0
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"database_tables_rechecked": 219,
|
||||
"tables_changed_since_backup": [
|
||||
"public.sessions"
|
||||
],
|
||||
"release_files_rechecked": 9873,
|
||||
"release_files_changed": [],
|
||||
"installed_revision": "96f510965f70581a4b41dd94463f598b77dd7cf5",
|
||||
"active_data_writes": 0,
|
||||
"source_read_only_transactions": true
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
[
|
||||
{
|
||||
"file": "/var/www/deploy/investmoz-pipeline.py",
|
||||
"range": [
|
||||
190,
|
||||
213
|
||||
],
|
||||
"source": "190: (release/'storage'/directory).mkdir(parents=True,exist_ok=True)\n191: with self.qa_database():\n192: self.run(['composer','install','--no-interaction','--prefer-dist','--no-scripts'],cwd=release,env=env)\n193: self.run([self.c['php'],'artisan','package:discover','--ansi'],cwd=release,env=env)\n194: # The guard is verified against the actual PostgreSQL identity, not only environment strings.\n195: guard = \"require 'vendor/autoload.php'; $app=require 'bootstrap/app.php'; $app->make(Illuminate\\\\Contracts\\\\Console\\\\Kernel::class)->bootstrap(); if (!App\\\\Support\\\\IsolatedQaDatabase::allowsSchemaChanges()) exit(91);\"\n196: self.run([self.c['php'],'-r',guard],cwd=release,env=env)\n197: # The project intentionally gates later schemas behind nominal authenticated\n198: # installation. Reuse its accepted isolated baseline lifecycle test instead\n199: # of bypassing those guards with a generic migrate of every module.\n200: self.run([self.c['php'],'vendor/bin/phpunit','--configuration=phpunit.investmoz.xml','--do-not-cache-result'],cwd=release,env=env)\n201: self.run(['npm','ci','--no-audit','--no-fund'],cwd=release,env=env)\n202: self.run(['npm','run','build'],cwd=release,env=env)\n203: self.run(['npm','run','types:check'],cwd=release,env=env)\n204: self.run([self.c['php'],'tools/portal-diagnostics-deployment-guard.php'],cwd=release,env=env)\n205: # Unit fixtures mock DB/mail and select their own feature settings.\n206: # Do not inject the destination's enabled-module flags into those mocks.\n207: unit_env={k:v for k,v in env.items() if not k.startswith(('PORTAL_','INVESTMOZ_'))}\n208: self.run([self.c['php'],'vendor/bin/phpunit','--configuration=phpunit.xml','--testsuite','Unit','--do-not-cache-result'],cwd=release,env=unit_env)\n209: tests=sorted((release/'tests/frontend').glob('*.test.mjs'))\n210: self.run(['node','--test',*tests],cwd=release,env=env)\n211: # Final production vendor from the same lock, before linking any real configuration.\n212: self.run(['composer','install','--no-dev','--no-scripts','--no-interaction','--prefer-dist','--optimize-autoloader'],cwd=release,env=env)\n213: self.run(['composer','check-platform-reqs','--no-dev'],cwd=release,env=env)"
|
||||
},
|
||||
{
|
||||
"file": "/var/www/deploy/investmoz-pipeline.py",
|
||||
"range": [
|
||||
238,
|
||||
277
|
||||
],
|
||||
"source": "238: 'built_at':datetime.datetime.now(datetime.timezone.utc).isoformat(),\n239: 'manifest_sha256':digest(release/'public/build/manifest.json')})\n240: for directory in [release/'bootstrap/cache']:\n241: os.chmod(directory,0o2770)\n242: \n243: def health(self, target: str) -> None:\n244: request=urllib.request.Request(self.c['target_url']+'/build/revision.json',headers={'User-Agent':'Mozilla/5.0 InvesteMoz-Delivery','Cache-Control':'no-cache'})\n245: with urllib.request.urlopen(request,timeout=20) as response:info=json.load(response)\n246: if info.get('commit')!=target:raise RuntimeError('Served commit differs from built commit')\n247: request=urllib.request.Request(self.c['target_url']+'/',headers={'User-Agent':'Mozilla/5.0 InvesteMoz-Delivery','Cache-Control':'no-cache'})\n248: with urllib.request.urlopen(request,timeout=20) as response:\n249: if response.status!=200 or response.headers.get('WWW-Authenticate'):raise RuntimeError('Public access regression')\n250: \n251: def deliver(self, target: str) -> dict:\n252: installed_file=self.state/'installed.json'\n253: installed=json.loads(installed_file.read_text())\n254: if installed.get('commit')==target:\n255: refuse_drift(self.current.resolve(),installed['hashes'])\n256: return {'status':'already_installed','commit':target}\n257: refuse_drift(self.current.resolve(),installed['hashes'])\n258: self.releases.mkdir(parents=True,exist_ok=True)\n259: if shutil.disk_usage(self.releases).free < 5 * 1024**3:\n260: raise RuntimeError('Insufficient free space for an isolated build; active release preserved')\n261: release=self.releases/(target+'-'+uuid.uuid4().hex[:8]);release.mkdir(mode=0o750)\n262: self.build(target,release)\n263: if self.latest()!=target:\n264: latest=self.git('rev-parse','refs/heads/delivery-target')\n265: write_json(self.state/'requests'/(latest+'.json'),{'commit':latest,'reason':'newer integration during build'})\n266: return {'status':'superseded','commit':target,'newer_commit':latest}\n267: # A programmer may have edited the served release while CI was running.\n268: # Recheck immediately before the atomic switch, preserving that work.\n269: refuse_drift(self.current.resolve(),installed['hashes'])\n270: previous=promote(self.current,release,lambda:self.health(target))\n271: try:\n272: self.run([self.c['php'],str(release/'artisan'),'queue:restart'],cwd=release)\n273: write_json(self.state/'previous.json',installed)\n274: info={'status':'installed','commit':target,'release':str(release),'previous_release':str(previous),\n275: 'hashes':tree_hashes(release),'database_migrations':0,'content_imports':0}\n276: write_json(installed_file,info)\n277: except Exception:"
|
||||
},
|
||||
{
|
||||
"file": "/var/www/deploy/investmoz-pipeline.py",
|
||||
"range": [
|
||||
287,
|
||||
308
|
||||
],
|
||||
"source": "287: fcntl.flock(lock,fcntl.LOCK_EX)\n288: succeeded=True\n289: for _ in range(3):\n290: requests=sorted((self.state/'requests').glob('*.json'))\n291: if not requests:return succeeded\n292: for path in requests:\n293: if not SHA.fullmatch(path.stem):raise RuntimeError('Unexpected queue entry')\n294: path.rename(self.state/'processed'/(path.stem+'-'+uuid.uuid4().hex[:8]+'.json'))\n295: target=None;report={}\n296: with (self.state/'logs'/(str(time.time_ns())+'.log')).open('wb') as self.log:\n297: try:\n298: target=self.latest();report=self.deliver(target)\n299: except Exception as error:\n300: succeeded=False\n301: report={'status':'failed','commit':target,'error':str(error),'active_release_preserved':True}\n302: finally:self.log=None\n303: report.update(requested_commits=[p.stem for p in requests],finished_at=datetime.datetime.now(datetime.timezone.utc).isoformat())\n304: write_json(self.state/'reports'/((target or 'fetch-failed')+'-'+str(time.time_ns())+'.json'),report)\n305: print(json.dumps(report),flush=True)\n306: return succeeded\n307: \n308: def rollback(self) -> dict:"
|
||||
},
|
||||
{
|
||||
"file": "/var/www/deploy/investmoz-pipeline.py",
|
||||
"range": [
|
||||
310,
|
||||
327
|
||||
],
|
||||
"source": "310: with (self.state/'delivery.lock').open('a') as lock:\n311: fcntl.flock(lock,fcntl.LOCK_EX)\n312: installed=json.loads((self.state/'installed.json').read_text())\n313: previous=json.loads((self.state/'previous.json').read_text())\n314: refuse_drift(self.current.resolve(),installed['hashes'])\n315: prior=Path(previous['release'])\n316: refuse_drift(prior,previous['hashes'])\n317: current=self.current.resolve()\n318: promote(self.current,prior,lambda:self.health(previous['commit']))\n319: try:\n320: self.run([self.c['php'],str(prior/'artisan'),'queue:restart'],cwd=prior)\n321: write_json(self.state/'installed.json',previous)\n322: write_json(self.state/'previous.json',installed)\n323: except Exception:\n324: point_current(self.current,current)\n325: write_json(self.state/'installed.json',installed)\n326: raise\n327: report={'status':'rolled_back','commit':previous['commit'],'release':str(prior),"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,152 @@
|
||||
[
|
||||
{
|
||||
"executor_entrypoints": [
|
||||
"/var/www/deploy/investmoz-pipeline.py",
|
||||
"/etc/investmoz-deploy/delivery.json"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "/var/www/deploy/investmoz-pipeline.py",
|
||||
"operations": [
|
||||
{
|
||||
"line": 4,
|
||||
"code": "Runs as the existing deployment OS user. Never resets the served checkout,"
|
||||
},
|
||||
{
|
||||
"line": 33,
|
||||
"code": "folders[:] = [name for name in folders if not (parent/name).is_symlink()"
|
||||
},
|
||||
{
|
||||
"line": 38,
|
||||
"code": "if path.is_symlink() or rel=='.env' or rel.startswith('public/build/assets/'):continue"
|
||||
},
|
||||
{
|
||||
"line": 55,
|
||||
"code": "if item.name == '.env' or item.name == 'public/hot' or (item.isfile()"
|
||||
},
|
||||
{
|
||||
"line": 72,
|
||||
"code": "if link.exists() and not link.is_symlink():"
|
||||
},
|
||||
{
|
||||
"line": 73,
|
||||
"code": "raise RuntimeError('Current path is not a symlink; existing directory preserved')"
|
||||
},
|
||||
{
|
||||
"line": 75,
|
||||
"code": "temporary.symlink_to(target, target_is_directory=True)"
|
||||
},
|
||||
{
|
||||
"line": 79,
|
||||
"code": "previous = link.resolve() if link.is_symlink() else None"
|
||||
},
|
||||
{
|
||||
"line": 121,
|
||||
"code": "self.git('fetch', '--no-tags', 'origin', '+refs/heads/'+branch+':refs/heads/delivery-target')"
|
||||
},
|
||||
{
|
||||
"line": 124,
|
||||
"code": "raise RuntimeError('Invalid fetched revision')"
|
||||
},
|
||||
{
|
||||
"line": 185,
|
||||
"code": "if not php.exists(): php.symlink_to(self.c['php'])"
|
||||
},
|
||||
{
|
||||
"line": 199,
|
||||
"code": "# of bypassing those guards with a generic migrate of every module."
|
||||
},
|
||||
{
|
||||
"line": 201,
|
||||
"code": "self.run(['npm','ci','--no-audit','--no-fund'],cwd=release,env=env)"
|
||||
},
|
||||
{
|
||||
"line": 202,
|
||||
"code": "self.run(['npm','run','build'],cwd=release,env=env)"
|
||||
},
|
||||
{
|
||||
"line": 203,
|
||||
"code": "self.run(['npm','run','types:check'],cwd=release,env=env)"
|
||||
},
|
||||
{
|
||||
"line": 214,
|
||||
"code": "if (release/'public/hot').exists(): raise RuntimeError('Development Vite marker must not ship')"
|
||||
},
|
||||
{
|
||||
"line": 225,
|
||||
"code": "(release/'storage').symlink_to(self.legacy/'storage',target_is_directory=True)"
|
||||
},
|
||||
{
|
||||
"line": 226,
|
||||
"code": "(release/'.env').symlink_to(self.legacy/'.env')"
|
||||
},
|
||||
{
|
||||
"line": 228,
|
||||
"code": "(release/'public/storage').symlink_to(self.legacy/'storage/app/public',target_is_directory=True)"
|
||||
},
|
||||
{
|
||||
"line": 252,
|
||||
"code": "installed_file=self.state/'installed.json'"
|
||||
},
|
||||
{
|
||||
"line": 265,
|
||||
"code": "write_json(self.state/'requests'/(latest+'.json'),{'commit':latest,'reason':'newer integration during build'})"
|
||||
},
|
||||
{
|
||||
"line": 272,
|
||||
"code": "self.run([self.c['php'],str(release/'artisan'),'queue:restart'],cwd=release)"
|
||||
},
|
||||
{
|
||||
"line": 287,
|
||||
"code": "fcntl.flock(lock,fcntl.LOCK_EX)"
|
||||
},
|
||||
{
|
||||
"line": 290,
|
||||
"code": "requests=sorted((self.state/'requests').glob('*.json'))"
|
||||
},
|
||||
{
|
||||
"line": 291,
|
||||
"code": "if not requests:return succeeded"
|
||||
},
|
||||
{
|
||||
"line": 292,
|
||||
"code": "for path in requests:"
|
||||
},
|
||||
{
|
||||
"line": 303,
|
||||
"code": "report.update(requested_commits=[p.stem for p in requests],finished_at=datetime.datetime.now(datetime.timezone.utc).isoformat())"
|
||||
},
|
||||
{
|
||||
"line": 304,
|
||||
"code": "write_json(self.state/'reports'/((target or 'fetch-failed')+'-'+str(time.time_ns())+'.json'),report)"
|
||||
},
|
||||
{
|
||||
"line": 311,
|
||||
"code": "fcntl.flock(lock,fcntl.LOCK_EX)"
|
||||
},
|
||||
{
|
||||
"line": 312,
|
||||
"code": "installed=json.loads((self.state/'installed.json').read_text())"
|
||||
},
|
||||
{
|
||||
"line": 320,
|
||||
"code": "self.run([self.c['php'],str(prior/'artisan'),'queue:restart'],cwd=prior)"
|
||||
},
|
||||
{
|
||||
"line": 321,
|
||||
"code": "write_json(self.state/'installed.json',previous)"
|
||||
},
|
||||
{
|
||||
"line": 325,
|
||||
"code": "write_json(self.state/'installed.json',installed)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "/etc/investmoz-deploy/delivery.json",
|
||||
"operations": []
|
||||
},
|
||||
{
|
||||
"pending_request_count": 0
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,30 @@
|
||||
set -eu
|
||||
python3 - <<'REMOTE'
|
||||
import json,pathlib,subprocess,os
|
||||
assert os.environ.get('SSH_CONNECTION')
|
||||
root=pathlib.Path('/var/www/html/teste-investidor.tempo.co.mz/current').resolve()
|
||||
php=r"""<?php
|
||||
require 'vendor/autoload.php';$app=require 'bootstrap/app.php';$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();
|
||||
use Illuminate\Support\Facades\DB;use Illuminate\Support\Facades\Schema;
|
||||
DB::beginTransaction();DB::statement('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY');
|
||||
try{
|
||||
$id=DB::selectOne('select current_database() as database,inet_server_port() as port');if($id->database!=='teste_investidor'||(int)$id->port!==5432)throw new RuntimeException('Wrong source');
|
||||
$tables=['opportunities'=>['management_version','workflow_context','owner_user_id','organization_id','management_draft'], 'users'=>['portal_access_revision'],'portal_role_assignments'=>['role','opportunity_types'],'portal_superuser_pair'=>[], 'portal_profile_requests'=>[], 'portal_security_approvals'=>[], 'opportunity_images'=>[], 'opportunity_documents'=>[], 'opportunity_locations'=>[], 'documents'=>['storage_encryption','malware_scan_status','malware_scanned_sha256'],'portal_editorial_editions'=>[], 'portal_editorial_notifications'=>['opportunity_id','opportunity_version','content_hash'],'audit_events'=>[], 'tasks'=>[], 'desk_cases'=>['payload','simulation_id'], 'investor_navigation_preferences'=>[], 'simulation_form_drafts'=>[]];
|
||||
$schema=[];foreach($tables as $table=>$columns){$exists=Schema::hasTable($table);$schema[$table]=['exists'=>$exists,'missing_columns'=>$exists?array_values(array_filter($columns,fn($c)=>!Schema::hasColumn($table,$c))):$columns];}
|
||||
$offer=Schema::hasColumn('opportunities','workflow_context')?DB::selectOne("select count(*) filter(where workflow_context->'private_offer_preparation' is not null) as private_offers,count(*) filter(where workflow_context->'private_offer_preparation' is not null and (status='published' or published_at is not null)) as unexpected_published,count(*) filter(where workflow_context->'offer_publication' is not null) as stored_projections from opportunities"):null;
|
||||
$routes=[];foreach($app->make('router')->getRoutes() as $route){if(str_contains($route->uri(),'ofertas')||str_contains($route->uri(),'offers'))$routes[]=['uri'=>$route->uri(),'methods'=>$route->methods(),'action'=>$route->getActionName()];}
|
||||
$recipients=config('portal.notification_recipients',[]);sort($recipients);
|
||||
echo json_encode(['database'=>$id,'php'=>PHP_VERSION,'gd'=>extension_loaded('gd'),'imagecreatefromstring'=>function_exists('imagecreatefromstring'),'imagewebp'=>function_exists('imagewebp'),'schema'=>$schema,'offers'=>$offer,'offer_routes'=>$routes,'flags'=>['profile_access'=>(bool)config('portal.profile_access_enabled'),'editorial'=>(bool)config('editorial_workflow.enabled'),'notifications'=>(bool)config('portal.notifications_enabled'),'homologation_only'=>(bool)config('portal.homologation_only'),'homologation_access_mode'=>config('portal.homologation_access_mode'),'nda_present'=>config('private_offers.nda')!==null,'expiry_scheduler'=>(bool)config('private_offers.expiry_scheduler_enabled',false)],'mail'=>['mailer'=>config('mail.default'),'smtp_host_configured'=>(bool)config('mail.mailers.smtp.host'),'recipient_count'=>count($recipients),'recipient_set_sha256'=>hash('sha256',json_encode($recipients))],'runtime'=>['queue'=>config('queue.default'),'private_disk'=>config('filesystems.default'),'scanner_host_configured'=>(bool)config('services.malware.host'),'scanner_socket_configured'=>(bool)config('services.malware.socket')],'migration_count'=>DB::table('migrations')->count(),'config_cached'=>$app->configurationIsCached(),'operational_data_writes'=>0],JSON_THROW_ON_ERROR);
|
||||
}finally{DB::rollBack();}
|
||||
"""
|
||||
q=json.loads(subprocess.check_output(['php8.4'],input=php.encode(),cwd=root,timeout=90))
|
||||
info=subprocess.run(['php-fpm8.4','-i'],capture_output=True,text=True,timeout=20).stdout
|
||||
q['fpm_image_capabilities']={'gd_enabled':'GD Support => enabled' in info,'webp_enabled':'WebP Support => enabled' in info}
|
||||
units=subprocess.run(['systemctl','list-units','--type=service','--all','--plain','--no-legend'],capture_output=True,text=True,timeout=15).stdout.splitlines()
|
||||
q['service_units']=[' '.join(line.split()[:4]) for line in units if any(x in line.lower() for x in ['teste-investidor','investmoz','nginx','php8.4','postgresql','clamav'])]
|
||||
config=json.loads(pathlib.Path('/etc/investmoz-deploy/delivery.json').read_text());q['restore_image']=config.get('postgis_image');q['executor_config_keys']=sorted(config)
|
||||
installed=json.loads(pathlib.Path('/var/lib/investmoz-deploy/installed.json').read_text());q['installed_revision']=installed.get('commit')
|
||||
rev=root/'public/build/revision.json';q['served_build_revision']=json.loads(rev.read_text()).get('commit') if rev.exists() else None
|
||||
q['executor_service_state']=subprocess.run(['systemctl','is-active','teste-investidor-deploy.service'],capture_output=True,text=True).stdout.strip()
|
||||
print(json.dumps(q))
|
||||
REMOTE
|
||||
@@ -0,0 +1,15 @@
|
||||
set -eu
|
||||
python3 - <<'REMOTE'
|
||||
from pathlib import Path
|
||||
import subprocess,json,hashlib,os
|
||||
assert os.environ.get('SSH_CONNECTION')
|
||||
root=Path('/var/www/html/teste-investidor.tempo.co.mz/current').resolve()
|
||||
stage=Path('/var/backups/investmoz-offers-preinstall-20261114/backup')
|
||||
php='<?php\nrequire \'vendor/autoload.php\';$app=require \'bootstrap/app.php\';$app->make(Illuminate\\Contracts\\Console\\Kernel::class)->bootstrap();\nuse Illuminate\\Support\\Facades\\DB;\nDB::beginTransaction(); DB::statement(\'SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY\'); DB::statement("SET TIME ZONE \'UTC\'");\ntry {\n $identity=DB::selectOne(\'select current_database() as database,inet_server_port() as port\');\n if($identity->database!==\'teste_investidor\'||(int)$identity->port!==5432)throw new RuntimeException(\'Wrong active source\');\n $snapshot=DB::selectOne(\'select pg_export_snapshot() as id\')->id;\n $tables=[];$sql="BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY; SET TRANSACTION SNAPSHOT \'".$snapshot."\'; SET TIME ZONE \'UTC\';\\n";\n foreach(DB::select("select schemaname,tablename from pg_tables where schemaname not in (\'pg_catalog\',\'information_schema\') and schemaname not like \'pg_%\' order by schemaname,tablename") as $t){\n $name=$t->schemaname.\'.\'.$t->tablename;$quoted=\'"\'.str_replace(\'"\',\'""\',$t->schemaname).\'"."\'.str_replace(\'"\',\'""\',$t->tablename).\'"\';\n $sql.="select json_build_object(\'name\',\'".$name."\',\'rows\',count(*),\'checksum\',md5(coalesce(string_agg(row_to_json(t)::text,E\'\\\\n\' order by md5(row_to_json(t)::text) COLLATE \\"C\\"),\'\'))) from ".$quoted." t;\\n";\n }\n $sql.=\'ROLLBACK;\';$reader=new Symfony\\Component\\Process\\Process([\'runuser\',\'-u\',\'postgres\',\'--\',\'psql\',\'-X\',\'-q\',\'-A\',\'-t\',\'-v\',\'ON_ERROR_STOP=1\',\'-d\',\'teste_investidor\']);$reader->setInput($sql)->setTimeout(90)->mustRun();\n foreach(explode("\\n",trim($reader->getOutput())) as $line){$row=json_decode($line,true,512,JSON_THROW_ON_ERROR);$name=$row[\'name\'];unset($row[\'name\']);$tables[$name]=$row;}\n echo json_encode([\'target\'=>$identity,\'tables\'=>$tables],JSON_THROW_ON_ERROR);\n}finally{DB::rollBack();}\n'
|
||||
actual=json.loads(subprocess.check_output(['php8.4'],input=php.encode(),cwd=root,timeout=120))
|
||||
receipt=json.loads((stage/'backup-receipt.json').read_text())
|
||||
changes=[name for name in set(receipt['tables'])|set(actual['tables']) if receipt['tables'].get(name)!=actual['tables'].get(name)]
|
||||
files=json.loads((stage/'release-files-private.json').read_text());code_changes=[name for name,want in files.items() if not (root/name).is_file() or hashlib.sha256((root/name).read_bytes()).hexdigest()!=want]
|
||||
proof={'database_tables_rechecked':len(actual['tables']),'tables_changed_since_backup':sorted(changes),'release_files_rechecked':len(files),'release_files_changed':code_changes,'installed_revision':json.loads(Path('/var/lib/investmoz-deploy/installed.json').read_text())['commit'],'active_data_writes':0,'source_read_only_transactions':True}
|
||||
(stage/'preservation-receipt.json').write_text(json.dumps(proof,indent=2));print(json.dumps(proof))
|
||||
REMOTE
|
||||
@@ -0,0 +1,86 @@
|
||||
{
|
||||
"verdict": "APTO COM CONDIÇÕES",
|
||||
"scope": "controlled private homologation; no operational publication",
|
||||
"installed_revision": "96f510965f70581a4b41dd94463f598b77dd7cf5",
|
||||
"accepted_integration_candidate": "9a014268",
|
||||
"candidate_including_preview_menu_delta": "3558647e8d82092cd0e31eefbe8b9e5e9eacf573",
|
||||
"installation_authorized": false,
|
||||
"installation_performed": false,
|
||||
"operational_migrations": false,
|
||||
"operational_data_or_permissions_or_configuration_changes": false,
|
||||
"real_email_sent": false,
|
||||
"backup": {
|
||||
"stage": "/var/backups/investmoz-offers-preinstall-20261114/backup",
|
||||
"created_at": "2026-11-14T17:34:37Z",
|
||||
"target": {
|
||||
"database": "teste_investidor",
|
||||
"port": 5432
|
||||
},
|
||||
"tables": 219,
|
||||
"files_verified": 417,
|
||||
"dump_sha256": "662c5d9f1fe7e01a5ccf7529c8c8e8f27205907d60d2da75a580b7b6c37c252e",
|
||||
"files_sha256": "f438f31f212e516442526b609bf5ac02cfd2f630a6ac2a2b6fc12f1f88717a42",
|
||||
"restore_verified": false
|
||||
},
|
||||
"restore": {
|
||||
"restore_verified": true,
|
||||
"all_table_counts_and_checksums_match": true,
|
||||
"tables": 219,
|
||||
"isolated_network": "none",
|
||||
"isolated_database": "offers_preinstall_restore_20261114_test",
|
||||
"isolated_port": 55441,
|
||||
"schema_changes": 0,
|
||||
"operational_data_writes": 0,
|
||||
"files_restored_and_verified": 417,
|
||||
"encryption": {
|
||||
"encrypted_originals_recovered": 0,
|
||||
"mfa_envelopes_recovered": 2,
|
||||
"preserved_keys_verified": true
|
||||
}
|
||||
},
|
||||
"dependencies_missing": [],
|
||||
"gd_cli_and_fpm_available": true,
|
||||
"in_memory_codec_passed": true,
|
||||
"destination_offer_routes": [],
|
||||
"destination_offer_counts": {
|
||||
"private_offers": 0,
|
||||
"unexpected_published": 0,
|
||||
"stored_projections": 0
|
||||
},
|
||||
"notifications_enabled": false,
|
||||
"authorized_email_recipient_count": 0,
|
||||
"condition_for_exact_revision": "Existing executor follows latest main, not requested SHA. Exclusive integration window with only the expressly authorised revision is required; otherwise do not enqueue/push. No executor changes were made.",
|
||||
"remaining_conditions": [
|
||||
"explicit installation authorisation naming candidate and menu delta",
|
||||
"recheck installed revision, main head, empty deploy queue and current backup before actual installation",
|
||||
"preserve operational publication/NDA/scheduler closure and notifications disabled",
|
||||
"desktop/mobile authenticated acceptance pending",
|
||||
"real controlled mailbox not designated; no real-email test yet",
|
||||
"NDA, property-proof and legal-publication decisions pending"
|
||||
],
|
||||
"guarded_restore": "new container, pinned existing image, network none, no published port, removed after verification",
|
||||
"evidence_sha256": {
|
||||
"backup-reviewed.sh": "ddea0084d9cc2e593e4b2836d27a39fac22f5bfa20e9432545de48d56c5a9a71",
|
||||
"backup-summary.json": "ca1f9fd3aebbf26059a9b015e71a3864172a3fe1e479b3ea57d97a6e3807167c",
|
||||
"constraints-automatics.json": "5b3a1b979c7fd06d4f745ed3814115ba4727982ea0b7e321226499bdfcda163f",
|
||||
"data-preservation.json": "a27f55d2e75ef8f14981d126482cb1d33b24a7132400f22f914d8dc51f5f389f",
|
||||
"executor-operations.json": "448d1abbe9ed6c44b775a09dd7198e1915bd4cd388e241213221d96b5cac96bb",
|
||||
"executor-status.json": "3cac96822ff3a24d135ba12dca93ba2203f6b308f3e465ba38600d7045899efc",
|
||||
"preflight-reviewed.sh": "7a0c701cfa27345268475594604f0d9db19037358e4a55cd6b6151b8b34cc199",
|
||||
"preservation-reviewed.sh": "d735500fb90581a73839478f1e3642584470a79368f69a8bebc9b251853b30d1",
|
||||
"restore-reviewed.sh": "4b2f16350f65b01e9105c5f0a31bad7b4bcaaf92cf67fb296903ca55c1f9b6ad",
|
||||
"restore-summary.json": "c9e96c7fd4eb94b02f9d80508970ceb5413510910ad86e37d12f3edeb928b82f",
|
||||
"security-runtime.json": "564da2cd07c0937687bb579c2e91395b4f598af2f3e765ca9122f39f495df7f2",
|
||||
"session-reconciliation.json": "caa6dc326d1e0dfc6d0c594ea96e935f9303d39b1fb6b8871ef97fe9ca4c2008",
|
||||
"target-final-preservation.json": "e6e6daccdef1568b93f889312d76c5d651aec3a3b85231624ac832385a0a4d35",
|
||||
"target-identity.json": "b276128c113ab7b34a6c9ab8f08f5a39a5d8d710f94dbd6e597a3283e6877eb2",
|
||||
"target-runtime-schema.json": "c6fce61c40a791dae53c625592b6d8f8b9457ca8a6f0de99bf1c2c9b5a6dc876"
|
||||
},
|
||||
"preservation": {
|
||||
"unchanged_tables": 218,
|
||||
"concurrent_new_sessions": 1,
|
||||
"prior_sessions_unchanged": 51,
|
||||
"release_files_unchanged": 9873
|
||||
},
|
||||
"evidence_hash_encoding": "SHA256 of UTF-8 text with Git LF normalisation"
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
set -eu
|
||||
python3 - <<'PY'
|
||||
from pathlib import Path
|
||||
import subprocess,json,time,uuid,os
|
||||
assert os.environ.get('SSH_CONNECTION')
|
||||
stage=Path('/var/backups/investmoz-offers-preinstall-20261114/backup')
|
||||
receipt=json.loads((stage/'backup-receipt.json').read_text())
|
||||
config=json.loads((stage/'delivery.json').read_text())
|
||||
name='investmoz-preservation-restore-'+uuid.uuid4().hex[:10]
|
||||
def run(args,**kw):return subprocess.check_output(args,stderr=subprocess.STDOUT,**kw)
|
||||
run(['docker','image','inspect',config['postgis_image']]) # Reuse an existing pinned image; never pull software.
|
||||
cid=run(['docker','run','-d','--name',name,'--label','investmoz.task=offers-preinstall-20261114','--network','none','--memory','768m','--env','POSTGRES_HOST_AUTH_METHOD=trust','--env','POSTGRES_DB=offers_preinstall_restore_20261114_test',config['postgis_image'],'postgres','-p','55441']).decode().strip()
|
||||
assert len(cid)==64
|
||||
try:
|
||||
for _ in range(40):
|
||||
if subprocess.run(['docker','exec',cid,'pg_isready','-p','55441','-U','postgres'],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL).returncode==0:break
|
||||
time.sleep(1)
|
||||
else:raise RuntimeError('Restore database not ready')
|
||||
def sql(text):return run(['docker','exec','-i',cid,'psql','-p','55441','-U','postgres','-d','offers_preinstall_restore_20261114_test','-X','-q','-A','-t','-v','ON_ERROR_STOP=1'],input=text.encode()).decode()
|
||||
assert sql('select current_database(),inet_server_port(),current_setting(\'port\');').strip()=='offers_preinstall_restore_20261114_test||55441'
|
||||
# Preserve policy role names, without copying credentials or grants into the test instance.
|
||||
roles=run(['runuser','-u','postgres','--','psql','-XAt','-d','teste_investidor','-c',"select rolname from pg_roles where rolname !~ '^pg_' and rolname <> 'postgres'"]).decode().splitlines()
|
||||
for role in roles:sql('CREATE ROLE "'+role.replace('"','""')+'" NOLOGIN;')
|
||||
run(['docker','cp',str(stage/'database.dump'),cid+':/tmp/database.dump'])
|
||||
run(['docker','exec',cid,'pg_restore','-p','55441','-U','postgres','-d','offers_preinstall_restore_20261114_test','--no-owner','--no-privileges','--exit-on-error','/tmp/database.dump'])
|
||||
text="SET TIME ZONE 'UTC';\n"
|
||||
for table in receipt['tables']:
|
||||
quoted='.'.join('"'+part.replace('"','""')+'"' for part in table.split('.',1))
|
||||
text+="select json_build_object('name','"+table+"','rows',count(*),'checksum',md5(coalesce(string_agg(row_to_json(t)::text,E'\\n' order by md5(row_to_json(t)::text) COLLATE \"C\"),''))) from "+quoted+" t;\n"
|
||||
restored={}
|
||||
for line in sql(text).splitlines():
|
||||
row=json.loads(line);table=row.pop('name');restored[table]=row
|
||||
assert restored==receipt['tables'], 'Restore rows/checksums differ'
|
||||
|
||||
# Extract regular files only into a new private directory, never into the active release.
|
||||
import tarfile,hashlib,shutil
|
||||
files=stage/'restore-files-rehearsal'
|
||||
files.mkdir(mode=0o700)
|
||||
recovered=0
|
||||
with tarfile.open(stage/'files-before.tar.gz','r:gz') as archive:
|
||||
for member in archive:
|
||||
if not member.isfile():continue
|
||||
part=Path(member.name)
|
||||
assert not part.is_absolute() and '..' not in part.parts
|
||||
assert member.name in receipt['file_manifest']
|
||||
destination=files/part;destination.parent.mkdir(mode=0o700,parents=True,exist_ok=True)
|
||||
with archive.extractfile(member) as source, destination.open('xb') as output:shutil.copyfileobj(source,output)
|
||||
expected=receipt['file_manifest'][member.name]
|
||||
assert destination.stat().st_size==expected['bytes'] and hashlib.sha256(destination.read_bytes()).hexdigest()==expected['sha256']
|
||||
recovered+=1
|
||||
assert recovered==receipt['file_count']
|
||||
documents=json.loads(sql("select coalesce(json_agg(row_to_json(d)),'[]'::json) from (select storage_key,storage_encryption,sha256 from documents where deleted_at is null and storage_encryption='laravel' and storage_key is not null) d;").strip())
|
||||
mfa=json.loads(sql("select coalesce(json_agg(row_to_json(u)),'[]'::json) from (select two_factor_secret,two_factor_recovery_codes from users where two_factor_secret is not null) u;").strip())
|
||||
(files/'encrypted-records.json').write_text(json.dumps({'documents':documents,'mfa':mfa}))
|
||||
root=Path('/var/www/html/teste-investidor.tempo.co.mz/current').resolve()
|
||||
php=r"""<?php
|
||||
require 'vendor/autoload.php';$app=require 'bootstrap/app.php';$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();
|
||||
$files='/var/backups/investmoz-offers-preinstall-20261114/backup/restore-files-rehearsal';
|
||||
$env=Dotenv\Dotenv::parse(file_get_contents($files.'/configuration/.env'));
|
||||
$keys=array_values(array_unique(array_filter([$env['APP_KEY']??null,...explode(',',$env['APP_PREVIOUS_KEYS']??'')])));
|
||||
$encrypters=array_map(fn($key)=>new Illuminate\Encryption\Encrypter(str_starts_with($key,'base64:')?base64_decode(substr($key,7),true):$key,config('app.cipher')),$keys);
|
||||
if(!$encrypters)throw new RuntimeException('Preserved encryption configuration missing');
|
||||
$decrypt=function($bytes)use($encrypters){foreach($encrypters as $encrypter){try{return $encrypter->decryptString($bytes);}catch(Throwable $error){}}throw new RuntimeException('Preserved envelope cannot be recovered');};
|
||||
$records=json_decode(file_get_contents($files.'/encrypted-records.json'),true,512,JSON_THROW_ON_ERROR);
|
||||
$source=realpath(base_path('storage/app'));$disk=realpath(config('filesystems.disks.local.root'));
|
||||
if(!$source||!$disk||!str_starts_with($disk,$source))throw new RuntimeException('Unexpected private disk mapping');
|
||||
$relative=ltrim(substr($disk,strlen($source)),'/');$count=0;$mfa=0;
|
||||
foreach($records['documents'] as $doc){$key=$doc['storage_key'];if(str_contains($key,'..')||str_starts_with($key,'/'))throw new RuntimeException('Unsafe storage key');$path=realpath($files.'/storage/app/'.$relative.'/'.$key);if(!$path||!str_starts_with($path,$files.'/storage/app/'))throw new RuntimeException('Missing recovered original');$bytes=$decrypt(file_get_contents($path));if(!hash_equals(trim($doc['sha256']),hash('sha256',$bytes)))throw new RuntimeException('Recovered original hash mismatch');$count++;}
|
||||
foreach($records['mfa'] as $row){foreach(['two_factor_secret','two_factor_recovery_codes'] as $field){if(!empty($row[$field]))$decrypt($row[$field]);}$mfa++;}
|
||||
echo json_encode(['encrypted_originals_recovered'=>$count,'mfa_envelopes_recovered'=>$mfa,'preserved_keys_verified'=>true],JSON_THROW_ON_ERROR);
|
||||
"""
|
||||
envelope=json.loads(subprocess.check_output(['php8.4'],input=php.encode(),cwd=root))
|
||||
proof={'restore_verified':True,'all_table_counts_and_checksums_match':True,'tables':len(restored),'isolated_network':'none','isolated_database':'offers_preinstall_restore_20261114_test','isolated_port':55441,'schema_changes':0,'operational_data_writes':0,'files_restored_and_verified':recovered,'encryption':envelope}
|
||||
(stage/'restore-receipt.json').write_text(json.dumps(proof,indent=2))
|
||||
print(json.dumps(proof))
|
||||
finally:
|
||||
details=json.loads(run(['docker','inspect',cid]))[0]
|
||||
assert details['Config']['Labels'].get('investmoz.task')=='offers-preinstall-20261114' and details['HostConfig']['NetworkMode']=='none'
|
||||
run(['docker','rm','-f','-v',cid])
|
||||
PY
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"restore_verified": true,
|
||||
"all_table_counts_and_checksums_match": true,
|
||||
"tables": 219,
|
||||
"isolated_network": "none",
|
||||
"isolated_database": "offers_preinstall_restore_20261114_test",
|
||||
"isolated_port": 55441,
|
||||
"schema_changes": 0,
|
||||
"operational_data_writes": 0,
|
||||
"files_restored_and_verified": 417,
|
||||
"encryption": {
|
||||
"encrypted_originals_recovered": 0,
|
||||
"mfa_envelopes_recovered": 2,
|
||||
"preserved_keys_verified": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"environment": "local",
|
||||
"debug": false,
|
||||
"session_secure": true,
|
||||
"session_http_only": true,
|
||||
"session_same_site": "lax",
|
||||
"app_url": "https://teste-investidor.tempo.co.mz",
|
||||
"fpm_environment_overrides": [],
|
||||
"nginx_app_environment_overrides": []
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"prior_sessions": 51,
|
||||
"current_sessions": 52,
|
||||
"prior_identifiers_preserved": true,
|
||||
"removed_prior_sessions": 0,
|
||||
"new_sessions": 1,
|
||||
"changed_fields_counts": {},
|
||||
"operational_data_writes": 0,
|
||||
"conclusion": "Live session activity observed during a read-only verification; no session content or identifiers exported."
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"installed_revision_unchanged": "96f510965f70581a4b41dd94463f598b77dd7cf5",
|
||||
"preexisting_files_changed": [],
|
||||
"release_archive_sha256": "914bc32627a51ec9001d07e5ac31d5717719a4a7f93e43dc26189865962735ca",
|
||||
"release_regular_files_verified": 9873,
|
||||
"release_archive_bytes": 25862122,
|
||||
"backup_directory_mode": "0o700",
|
||||
"codec": {
|
||||
"in_memory_codec_passed": true,
|
||||
"original_files_touched": 0,
|
||||
"network_requests": 0
|
||||
},
|
||||
"executor": {
|
||||
"branch": "main",
|
||||
"current_link": "/var/www/html/teste-investidor.tempo.co.mz/current",
|
||||
"legacy_root": "/var/www/html/teste-investidor.tempo.co.mz",
|
||||
"php": "/usr/bin/php8.4",
|
||||
"release_root": "/var/www/investmoz-homologacao/releases",
|
||||
"repository": "nala/Investidor-V2",
|
||||
"retention": "No automatic deletion of releases or data",
|
||||
"schema_contract": "/etc/investmoz-deploy/schema-contract.json",
|
||||
"state_root": "/var/lib/investmoz-deploy",
|
||||
"target_url": "https://teste-investidor.tempo.co.mz"
|
||||
},
|
||||
"deploy_service": "inactive",
|
||||
"deploy_path": "active",
|
||||
"operational_writes": 0,
|
||||
"services_reconfigured": false
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"root": "/var/www/investmoz-homologacao/releases/96f510965f70581a4b41dd94463f598b77dd7cf5-8ee72062",
|
||||
"git_revision": "",
|
||||
"working_tree": [],
|
||||
"installed_revision": "96f510965f70581a4b41dd94463f598b77dd7cf5",
|
||||
"services": {
|
||||
"nginx": "active",
|
||||
"php8.4-fpm": "active",
|
||||
"teste-investidor-deploy.service": "inactive",
|
||||
"teste-investidor-deploy.path": "active"
|
||||
},
|
||||
"disk_free_bytes": 24556748800,
|
||||
"read_only": true
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user