['required', Rule::in([ OpportunityStatus::Submitted->value, OpportunityStatus::Triage->value, OpportunityStatus::InReview->value, OpportunityStatus::NeedsInformation->value, OpportunityStatus::ApprovalL1->value, OpportunityStatus::ApprovalL2->value, OpportunityStatus::Approved->value, OpportunityStatus::Published->value, OpportunityStatus::Rejected->value, OpportunityStatus::Withdrawn->value, ])], 'note' => ['required', 'string', 'min:10', 'max:2000'], ])->validate(); return DB::transaction(function () use ($actor, $id, $version, $action, $note, $consent, $confirmationCode) { $business = app(BusinessRepository::class)->find($id, true); $profileWorkflow = ExistingBusinessProfileCategory::category($business) !== null && app(PortalProfileAccess::class)->available(); $ability = match ($action) { OpportunityStatus::Submitted->value, OpportunityStatus::Withdrawn->value => 'owns', OpportunityStatus::Triage->value => 'triage', OpportunityStatus::ApprovalL2->value => 'approveLevelOne', OpportunityStatus::Approved->value => 'approveLevelTwo', OpportunityStatus::Published->value => 'publish', OpportunityStatus::Rejected->value => in_array($business->status, ['approval_l1', 'approval_l2'], true) ? ($business->status === 'approval_l1' ? 'approveLevelOne' : 'approveLevelTwo') : 'review', default => 'review', }; Gate::forUser($actor)->authorize($ability, $business); abort_unless($business->version_no === $version, 409, 'Versão desactualizada.'); $allowed = [ OpportunityStatus::Submitted->value => [OpportunityStatus::Draft->value, OpportunityStatus::NeedsInformation->value], OpportunityStatus::Triage->value => [OpportunityStatus::Submitted->value], OpportunityStatus::InReview->value => [OpportunityStatus::Triage->value], OpportunityStatus::NeedsInformation->value => [OpportunityStatus::Triage->value, OpportunityStatus::InReview->value], OpportunityStatus::ApprovalL1->value => [OpportunityStatus::InReview->value], OpportunityStatus::ApprovalL2->value => [OpportunityStatus::ApprovalL1->value], OpportunityStatus::Approved->value => [OpportunityStatus::ApprovalL2->value], OpportunityStatus::Published->value => [OpportunityStatus::Approved->value], OpportunityStatus::Rejected->value => [OpportunityStatus::Triage->value, OpportunityStatus::InReview->value, OpportunityStatus::ApprovalL1->value, OpportunityStatus::ApprovalL2->value], OpportunityStatus::Withdrawn->value => [OpportunityStatus::Draft->value, OpportunityStatus::Submitted->value, OpportunityStatus::Triage->value, OpportunityStatus::InReview->value, OpportunityStatus::NeedsInformation->value], ]; abort_unless(in_array($business->status, $allowed[$action], true), 409, 'Transição indisponível neste estado.'); // Reuse the bounded delivery contract: profile correspondence is access, not functional enablement. $category = ExistingBusinessProfileCategory::category($business); if ($category !== null && in_array($action, ['submitted', 'published'], true)) { $blockers = app(OpportunityCanonicalContract::class)->deliveryBlockers(['opportunity_category' => $category]); if ($blockers) { throw ValidationException::withMessages(['modality' => implode(' ', array_values($blockers))]); } } if ($action === 'submitted') { if (($business->payload['wizard_version'] ?? null) === '2') { BusinessFormSchema::validateSubmission($business->payload, $business->sector_id, $business->modality); $requiredKind = ['asset_sale' => 'asset', 'asset_lease' => 'asset', 'real_estate' => 'property', 'agricultural_investment' => 'agricultural_investment', 'land_right' => 'land_right', 'mining_license' => 'license', 'forestry_license' => 'license', 'capital_increase' => 'capital_request', 'participation_sale' => 'participation'][$business->modality] ?? null; if ($requiredKind !== null && ! ExistingBusinessObject::where('business_id', $business->id)->where('kind', $requiredKind)->exists()) { throw ValidationException::withMessages(['objects' => 'Declare o elemento aplicável na etapa 6: '.$requiredKind]); } Validator::make(['confirmation_code' => $confirmationCode, 'sector_id' => $business->sector_id], [ 'confirmation_code' => ['required', Rule::in([$business->code])], 'sector_id' => ['required', Rule::exists('sectors', 'id')->whereNull('deleted_at')->where('is_active', true)], ])->validate(); } elseif (($business->payload['wizard_version'] ?? null) === '1') { Validator::make($business->payload, [ 'proponent_type' => ['required'], 'proponent_name' => ['required'], 'tax_identifier' => ['required'], 'contact_email' => ['required', 'email'], 'country' => ['required', app(CountryCatalogue::class)->rule()], 'declared_origin_country_code' => ['nullable', app(CountryCatalogue::class)->rule()], 'acting_capacity' => ['required'], 'declared_holder' => ['required'], 'location_notes' => ['required'], 'legitimacy_declaration' => ['required', 'in:yes'], 'mandate_reference' => ['required_if:acting_capacity,representative'], 'mandate_valid_until' => ['required_if:acting_capacity,representative', 'nullable', 'date_format:Y-m-d', 'after_or_equal:today'], ])->validate(); Validator::make(['confirmation_code' => $confirmationCode], [ 'confirmation_code' => ['required', Rule::in([$business->code])], ])->validate(); } if (($business->payload['wizard_version'] ?? null) === '1') { $requiredKind = ['asset_sale' => 'asset', 'asset_lease' => 'asset', 'real_estate' => 'property', 'agricultural_investment' => 'agricultural_investment', 'land_right' => 'land_right', 'mining_license' => 'license', 'forestry_license' => 'license', 'capital_increase' => 'capital_request', 'participation_sale' => 'participation'][$business->modality] ?? null; if ($requiredKind !== null && ! ExistingBusinessObject::where('business_id', $business->id)->where('kind', $requiredKind)->exists()) { throw ValidationException::withMessages(['objects' => 'Declare the applicable object in step 6: '.$requiredKind]); } } $submissionRules = [ 'description' => ['required', 'string', 'min:30'], 'source_reference' => ['required', 'string', 'min:10'], 'consent' => ['accepted'], // Deadlines are optional for newer and legacy payloads, but // an explicitly supplied deadline must never be expired. 'valid_until' => ['nullable', 'date_format:Y-m-d', 'after_or_equal:today'], ]; if (($business->payload['wizard_version'] ?? null) === '1') { $submissionRules['valid_until'][0] = 'required'; } Validator::make([...$business->payload, 'consent' => $consent], $submissionRules)->validate(); } if ($action === OpportunityStatus::Approved->value) { $levelOneActor = DB::table('business_workflow_transitions')->where('existing_business_id', $business->id) ->where('to_status', OpportunityStatus::ApprovalL2->value)->orderByDesc('created_at')->value('actor_id'); abort_unless($levelOneActor !== null && ($profileWorkflow || (int) $levelOneActor !== (int) $actor->id), 403, 'A aprovação de nível 2 deve ser feita por uma pessoa diferente da aprovação de nível 1.'); } if ($action === OpportunityStatus::Published->value) { abort_unless(is_string($business->public_title) && trim($business->public_title) !== '' && $business->sector_id, 422, 'Preencha o título público e o sector antes de publicar.'); $approvalActors = DB::table('business_workflow_transitions')->where('existing_business_id', $business->id) ->whereIn('to_status', [OpportunityStatus::ApprovalL2->value, OpportunityStatus::Approved->value]) ->whereNotNull('actor_id')->pluck('actor_id')->map(fn ($id) => (int) $id)->all(); abort_unless(! in_array((int) $actor->id, $approvalActors, true), 403, 'A publicação deve ser executada por uma pessoa diferente dos aprovadores.'); } $fromStatus = $business->status; $business->status = $action; if ($action === OpportunityStatus::Published->value) { $business->published_at = now(); $business->public_slug ??= Str::slug($business->public_title).'-'.strtolower(substr(str_replace('-', '', $business->id), 0, 10)); } $business->version_no++; $business->save(); $correlationId = (string) Str::uuid(); DB::table('business_workflow_transitions')->insert([ 'id' => (string) Str::uuid(), 'existing_business_id' => $business->id, 'from_status' => $fromStatus, 'to_status' => $action, 'actor_id' => $actor->id, 'reason' => Crypt::encryptString($note), 'correlation_id' => $correlationId, 'metadata' => json_encode(['version_no' => $business->version_no, 'source' => 'portal-intake', 'authorization_mode' => $profileWorkflow ? 'profiles-v1-company-sale' : 'legacy-designations'], JSON_THROW_ON_ERROR), 'created_at' => now(), ]); if ($action === OpportunityStatus::Submitted->value) { DB::table('tasks')->insertOrIgnore([ 'source_system' => 'portal-intake', 'external_id' => 'BusinessListingTriage:'.$business->id.':'.$business->version_no, 'version_no' => 1, 'data_classification' => 'internal', 'correlation_id' => $correlationId, 'existing_business_id' => $business->id, 'task_type' => 'triage', 'title' => 'Triar oportunidade '.$business->code, 'status' => 'open', 'priority' => 3, 'due_at' => now()->addWeekdays(3), 'created_at' => now(), 'updated_at' => now(), ]); DB::table('outbox_events')->insert([ 'source_system' => 'portal-intake', 'external_id' => 'BusinessListingSubmitted:'.$business->id.':'.$business->version_no, 'aggregate_type' => 'business_listing', 'aggregate_id' => $business->id, 'event_type' => 'BusinessListingSubmitted', 'data_classification' => 'confidential', 'correlation_id' => $correlationId, 'status' => 'pending', 'payload' => json_encode(['business_id' => $business->id, 'version_no' => $business->version_no], JSON_THROW_ON_ERROR), ]); } DB::table('existing_business_reviews')->insert(['id' => (string) Str::uuid(), 'business_id' => $id, 'actor_id' => $actor->id, 'version_no' => $business->version_no, 'action' => $action, 'note' => Crypt::encryptString(json_encode(['note' => $note, 'review_consent' => $action === 'submitted' ? $consent : null], JSON_THROW_ON_ERROR))]); app(ExistingBusinessDrafts::class)->snapshot($business, $actor, $action, $note); return $business; }); } }