188 lines
12 KiB
PHP
188 lines
12 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Domain\Opportunities\Enums\OpportunityStatus;
|
|
use App\Models\ExistingBusiness;
|
|
use App\Models\ExistingBusinessObject;
|
|
use App\Models\User;
|
|
use App\Services\Mne\BusinessRepository;
|
|
use Illuminate\Support\Facades\Crypt;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Gate;
|
|
use Illuminate\Support\Facades\Validator;
|
|
use Illuminate\Support\Str;
|
|
use Illuminate\Validation\Rule;
|
|
use Illuminate\Validation\ValidationException;
|
|
|
|
final class ExistingBusinessIntake
|
|
{
|
|
public function transition(User $actor, string $id, int $version, string $action, string $note, bool $consent = false, ?string $confirmationCode = null): ExistingBusiness
|
|
{
|
|
Validator::make(compact('action', 'note'), [
|
|
'action' => ['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);
|
|
$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.');
|
|
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', 'regex:/^[A-Z]{2}$/'],
|
|
'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 && (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'], 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;
|
|
});
|
|
}
|
|
}
|