feat(offers): implement phase four private details and confidentiality in QA

This commit is contained in:
2026-11-14 17:28:12 +02:00
parent 7691cab3bc
commit e431de8b42
29 changed files with 2356 additions and 12 deletions
@@ -6,15 +6,84 @@ use App\Models\Opportunity;
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\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']);
@@ -40,7 +109,9 @@ final class PrivateOfferPreparationController extends Controller
$options = app(OpportunityCanonicalContract::class)->options($request->user(), $record);
return Inertia::render('offers/preparation', ['record' => $presentation, 'contexts' => $contexts,
'contacts' => $options['contacts'], 'contactInvestors' => $options['contactInvestors']]);
'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