163 lines
9.7 KiB
PHP
163 lines
9.7 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use App\Models\InvestmentSubmission;
|
|
use App\Models\User;
|
|
use App\Services\CountryCatalogue;
|
|
use App\Services\InvestmentWorkspace;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\RedirectResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Crypt;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Str;
|
|
use Inertia\Inertia;
|
|
use Inertia\Response;
|
|
|
|
class InvestmentWorkspaceController extends Controller
|
|
{
|
|
public function profile(Request $request, InvestmentWorkspace $workspace): Response|JsonResponse
|
|
{
|
|
$data = ['countries' => app(CountryCatalogue::class)->all(), 'account' => $workspace->account($request->user()->id), 'companies' => DB::table('investment_company_declarations')
|
|
->where('user_id', $request->user()->id)->orderByDesc('created_at')->get(['id', 'version', 'payload'])->map(function ($company) {
|
|
$company->payload = json_decode(Crypt::decryptString($company->payload), true, 512, JSON_THROW_ON_ERROR);
|
|
|
|
return $company;
|
|
})];
|
|
|
|
return $request->expectsJson() ? response()->json($data) : Inertia::render('investment/profile', $data);
|
|
}
|
|
|
|
public function saveProfile(Request $request): RedirectResponse|JsonResponse
|
|
{
|
|
$data = $request->validate(['version' => ['required', 'integer', 'min:0'],
|
|
'payload' => ['required', 'array:phone,country,residence_country_code,sector,experience,investment_capacity'],
|
|
'payload.residence_country_code' => ['nullable', 'string', app(CountryCatalogue::class)->rule()],
|
|
'payload.phone' => ['nullable', 'string', 'max:30'], 'payload.country' => ['nullable', 'string', 'max:100'],
|
|
'payload.sector' => ['nullable', 'string', 'max:150'], 'payload.experience' => ['nullable', 'string', 'max:2000'],
|
|
'payload.investment_capacity' => ['nullable', 'string', 'max:1000']]);
|
|
$version = DB::transaction(function () use ($request, $data) {
|
|
DB::table('users')->where('id', $request->user()->id)->lockForUpdate()->firstOrFail();
|
|
$row = DB::table('investment_accounts')->where('user_id', $request->user()->id)->first();
|
|
abort_unless((int) ($row->version ?? 0) === (int) $data['version'], 409, 'Perfil alterado. Recarregue.');
|
|
$version = (int) $data['version'] + 1;
|
|
DB::table('investment_accounts')->updateOrInsert(['user_id' => $request->user()->id], ['version' => $version,
|
|
'payload' => Crypt::encryptString(json_encode($data['payload'], JSON_THROW_ON_ERROR)), 'created_at' => $row->created_at ?? now(), 'updated_at' => now()]);
|
|
$this->audit($request, 'profile_saved', 'investment_accounts', null, ['version' => $version]);
|
|
|
|
return $version;
|
|
});
|
|
|
|
return $request->expectsJson() ? response()->json(['version' => $version]) : back()->with('success', 'Perfil de investimento guardado.');
|
|
}
|
|
|
|
public function company(Request $request, ?string $id = null): RedirectResponse|JsonResponse
|
|
{
|
|
$data = $request->validate(['version' => ['required', 'integer', 'min:0'],
|
|
'payload' => ['required', 'array:name,country,incorporation_country_code,registration,sector,role,description'],
|
|
'payload.incorporation_country_code' => ['nullable', 'string', app(CountryCatalogue::class)->rule()],
|
|
'payload.name' => ['required', 'string', 'min:2', 'max:200'], 'payload.country' => ['required_without:payload.incorporation_country_code', 'nullable', 'string', 'max:100'],
|
|
'payload.registration' => ['nullable', 'string', 'max:100'], 'payload.sector' => ['nullable', 'string', 'max:150'],
|
|
'payload.role' => ['required', 'string', 'max:150'], 'payload.description' => ['nullable', 'string', 'max:2000']]);
|
|
$result = DB::transaction(function () use ($request, $id, $data) {
|
|
DB::table('users')->where('id', $request->user()->id)->lockForUpdate()->firstOrFail();
|
|
$row = $id ? DB::table('investment_company_declarations')->where('id', $id)->where('user_id', $request->user()->id)->first() : null;
|
|
abort_if($id && ! $row, 404);
|
|
abort_unless((int) ($row->version ?? 0) === (int) $data['version'], 409, 'Declaração alterada. Recarregue.');
|
|
abort_if(! $id && DB::table('investment_company_declarations')->where('user_id', $request->user()->id)->count() >= 20, 422, 'Máximo de vinte empresas declaradas.');
|
|
$id ??= (string) Str::uuid();
|
|
$version = (int) $data['version'] + 1;
|
|
DB::table('investment_company_declarations')->updateOrInsert(['id' => $id], ['user_id' => $request->user()->id, 'version' => $version,
|
|
'payload' => Crypt::encryptString(json_encode($data['payload'], JSON_THROW_ON_ERROR)), 'created_at' => $row->created_at ?? now(), 'updated_at' => now()]);
|
|
$this->audit($request, 'company_declared', 'investment_company_declarations', $id, ['version' => $version]);
|
|
|
|
return ['id' => $id, 'version' => $version];
|
|
});
|
|
|
|
return $request->expectsJson() ? response()->json($result) : back()->with('success', 'Declaração de empresa guardada.');
|
|
}
|
|
|
|
public function notifications(Request $request): Response|JsonResponse
|
|
{
|
|
$query = DB::table('investment_notifications')->where('user_id', $request->user()->id);
|
|
if (! $request->user()->can('manage-portal')) {
|
|
$query->whereIn('submission_id', InvestmentSubmission::where('user_id', $request->user()->id)->select('id'));
|
|
}
|
|
$data = ['unread' => (clone $query)->whereNull('read_at')->count(), 'notifications' => $query->orderByDesc('created_at')->paginate(20)->through(function ($row) {
|
|
$row->payload = json_decode(Crypt::decryptString($row->payload), true, 512, JSON_THROW_ON_ERROR);
|
|
|
|
return $row;
|
|
})];
|
|
|
|
return $request->expectsJson() ? response()->json($data) : Inertia::render('investment/notifications', $data);
|
|
}
|
|
|
|
public function readNotification(Request $request, string $id): RedirectResponse|JsonResponse
|
|
{
|
|
$row = DB::table('investment_notifications')->where('id', $id)->where('user_id', $request->user()->id)->first();
|
|
abort_unless($row !== null, 404);
|
|
$this->submission($request, $row->submission_id);
|
|
DB::table('investment_notifications')->where('id', $id)->whereNull('read_at')->update(['read_at' => now()]);
|
|
|
|
return $request->expectsJson() ? response()->json(['read' => true]) : back();
|
|
}
|
|
|
|
public function message(Request $request, string $id, InvestmentWorkspace $workspace): RedirectResponse|JsonResponse
|
|
{
|
|
$data = $request->validate(['client_key' => ['required', 'uuid'], 'body' => ['required', 'string', 'min:3', 'max:5000']]);
|
|
$messageId = DB::transaction(function () use ($request, $id, $data, $workspace) {
|
|
$submission = $this->submission($request, $id, true);
|
|
abort_unless($submission->status === 'submitted', 409, 'Submeta a manifestação antes de enviar mensagens.');
|
|
$key = ['submission_id' => $id, 'actor_id' => $request->user()->id, 'client_key' => $data['client_key']];
|
|
$existing = DB::table('investment_messages')->where($key)->first();
|
|
if ($existing) {
|
|
abort_unless(hash_equals(Crypt::decryptString($existing->body), $data['body']), 409, 'Chave já usada para outra mensagem.');
|
|
|
|
return $existing->id;
|
|
}
|
|
$isOwner = (int) $submission->user_id === $request->user()->id;
|
|
$messageId = (string) Str::uuid();
|
|
DB::table('investment_messages')->insert([...$key, 'id' => $messageId, 'sender_role' => $isOwner ? 'investor' : 'support',
|
|
'body' => Crypt::encryptString($data['body']), 'created_at' => now()]);
|
|
$recipients = $isOwner ? User::whereIn(DB::raw('lower(email)'), config('portal.managers', []))->whereNotNull('email_verified_at')->pluck('id')->all() : [$submission->user_id];
|
|
foreach ($recipients as $recipient) {
|
|
if ((int) $recipient !== $request->user()->id) {
|
|
$workspace->notify((int) $recipient, $submission, 'message', $messageId);
|
|
}
|
|
}
|
|
$this->audit($request, 'message_sent', 'investment_messages', $messageId);
|
|
|
|
return $messageId;
|
|
});
|
|
|
|
return $request->expectsJson() ? response()->json(['id' => $messageId]) : back()->with('success', 'Mensagem registada na conversa.');
|
|
}
|
|
|
|
public function revision(Request $request, string $id, int $version): JsonResponse
|
|
{
|
|
$this->submission($request, $id);
|
|
$revision = DB::table('investment_submission_revisions')->where('submission_id', $id)->where('version', $version)->first();
|
|
abort_unless($revision !== null, 404);
|
|
|
|
return response()->json(['version' => $version, 'snapshot' => json_decode(Crypt::decryptString($revision->snapshot), true, 512, JSON_THROW_ON_ERROR)]);
|
|
}
|
|
|
|
private function submission(Request $request, string $id, bool $lock = false): InvestmentSubmission
|
|
{
|
|
$query = InvestmentSubmission::whereKey($id);
|
|
if (! $request->user()->can('manage-portal')) {
|
|
$query->where('user_id', $request->user()->id);
|
|
}
|
|
|
|
return ($lock ? $query->lockForUpdate() : $query)->firstOrFail();
|
|
}
|
|
|
|
/** @param array<string,mixed> $metadata */
|
|
private function audit(Request $request, string $action, string $entity, ?string $id, array $metadata = []): void
|
|
{
|
|
DB::table('audit_events')->insert(['source_system' => 'investment-workspace', 'actor_user_id' => $request->user()->id,
|
|
'entity_type' => $entity, 'entity_id' => $id, 'action' => $action, 'after_data' => json_encode($metadata, JSON_THROW_ON_ERROR)]);
|
|
}
|
|
}
|