From f5fe4e25a0c6c5c6838ab2d3511b12c7785af62f Mon Sep 17 00:00:00 2001 From: mac Date: Wed, 5 Aug 2026 21:06:56 +0300 Subject: [PATCH] Fix bi --- api/v2/app/src/controllers/DealController.php | 851 ++++++++++++++++++ engine/classes/BiAnalyticsLogger.php | 162 ++++ 2 files changed, 1013 insertions(+) create mode 100644 api/v2/app/src/controllers/DealController.php create mode 100644 engine/classes/BiAnalyticsLogger.php diff --git a/api/v2/app/src/controllers/DealController.php b/api/v2/app/src/controllers/DealController.php new file mode 100644 index 0000000..c653271 --- /dev/null +++ b/api/v2/app/src/controllers/DealController.php @@ -0,0 +1,851 @@ +validate([ + 'title' => 'required|string|max:255', + 'side_one' => 'required|string', + 'side_two' => 'required|string', + 'side_one_requisition_id' => 'nullable|integer|exists:requisitions,id', + 'side_two_requisition_id' => 'nullable|integer|exists:requisitions,id', + 'side_one_object_id' => 'nullable|integer', + 'side_two_object_id' => 'nullable|integer', + 'side_one_clients' => 'nullable|array', + 'side_two_clients' => 'nullable|array', + 'contract_price' => 'nullable|numeric', + 'object_price' => 'nullable|numeric', + 'commission_buyer' => 'nullable|numeric', + 'commission_seller' => 'nullable|numeric', + 'agent_commission' => 'nullable|numeric', + 'total_commission' => 'nullable|numeric', + 'sellers_count' => 'nullable|integer', + 'buyers_count' => 'nullable|integer', + 'proxy_sale' => 'nullable|boolean', + 'e_registration' => 'nullable|boolean', + 'mortgaged' => 'nullable|boolean', + 'mortgagee' => 'nullable|string', + 'debt_amount' => 'nullable|numeric', + 'has_counter_agent' => 'nullable|boolean', + 'counter_agent_name' => 'nullable|string', + 'counter_agent_commission' => 'nullable|numeric', + 'counter_agent_phone' => 'nullable|string', + 'deal_date' => 'nullable|date', + 'employees' => 'nullable|array', + 'employees.*' => 'integer|exists:users,id', + ]); + + $this->validateRequisitionsNotInDeal($validatedData['side_one_requisition_id'] ?? null); + $this->validateRequisitionsNotInDeal($validatedData['side_two_requisition_id'] ?? null); + + $deal = Deal::create($validatedData); + + if (!empty($validatedData['employees'])) { + $deal->users()->sync($validatedData['employees']); + } + + if (!empty($validatedData['side_one_requisition_id'])) { + Requisition::where('id', $validatedData['side_one_requisition_id']) + ->update(['deal_id' => $deal->id]); + + // Автоматическое добавление ответственных за сделку в партнёры заявки стороны 1 + if (isset($validatedData['employees'])) { + $sideOneReq = Requisition::find($validatedData['side_one_requisition_id']); + if ($sideOneReq) { + $whoWork = (int) $sideOneReq->who_work; + $currentUserId = auth()->id(); + + $doers = is_string($sideOneReq->doers) ? json_decode($sideOneReq->doers, true) : ($sideOneReq->doers ?? []); + if (!is_array($doers)) $doers = []; + + $newDoers = $doers; + $changed = false; + + foreach ($validatedData['employees'] as $employeeId) { + $employeeId = (int) $employeeId; + + if ($employeeId === $whoWork || $employeeId === $currentUserId) { + continue; + } + + if (array_key_exists($employeeId, $doers)) { + continue; + } + + $exists = EventClient::where('req_id', $validatedData['side_one_requisition_id']) + ->where('from_id', $employeeId) + ->where('event', 'transmitted_parallel') + ->exists(); + + if (!$exists) { + EventClient::create([ + 'user_id' => $whoWork ?: $currentUserId, + 'req_id' => $validatedData['side_one_requisition_id'], + 'from_id' => $employeeId, + 'client_id' => $sideOneReq->client_id, + 'event' => 'transmitted_parallel', + 'read' => 0, + 'send_telegramm' => 1, + ]); + + $newDoers[$employeeId] = 0; + $changed = true; + + DB::table('requisition_doers')->insert([ + 'requisition_id' => $validatedData['side_one_requisition_id'], + 'user_id' => $employeeId, + 'confirm' => 0, + ]); + } + } + + if ($changed) { + $sideOneReq->update([ + 'doers' => json_encode($newDoers), + 'doers_confirm' => 0, + ]); + } + } + } + } + + if (!empty($validatedData['side_two_requisition_id'])) { + Requisition::where('id', $validatedData['side_two_requisition_id']) + ->update(['deal_id' => $deal->id]); + } + + $clientEvents = []; + + // Получаем ID клиентов стороны 1 + $sideOneClientIds = []; + if (!empty($validatedData['side_one_clients'])) { + foreach ($validatedData['side_one_clients'] as $client) { + if (is_array($client) && isset($client['id'])) { + $sideOneClientIds[] = $client['id']; + } elseif (is_numeric($client)) { + $sideOneClientIds[] = (int) $client; + } + } + } + + // Добавляем клиента из заявки, если его нет в списке + $sideOneClientId = $validatedData['side_one_requisition_id'] + ? Requisition::where('id', $validatedData['side_one_requisition_id'])->value('client_id') + : null; + + if ($sideOneClientId && !in_array($sideOneClientId, $sideOneClientIds)) { + $sideOneClientIds[] = $sideOneClientId; + } + + // Получаем who_work для всех клиентов стороны 1 + $sideOneClientWhoWork = []; + if (!empty($sideOneClientIds)) { + $sideOneClientWhoWork = Client::whereIn('id', $sideOneClientIds) + ->pluck('who_work', 'id') + ->toArray(); + } + + // Создаем события для клиентов стороны 1 + if (!empty($validatedData['side_one_requisition_id']) && !empty($sideOneClientIds)) { + $defaultUserId = Requisition::where('id', $validatedData['side_one_requisition_id'])->value('who_work') ?: auth()->id(); + + foreach ($sideOneClientIds as $clientId) { + $userId = $sideOneClientWhoWork[$clientId] ?? $defaultUserId; + + if (empty($userId)) { + $userId = auth()->id(); + } + + $clientEvents[] = [ + 'user_id' => $userId, + 'client_id' => $clientId, + 'req_id' => 0, + 'type' => 'deal', + 'deal_id' => $deal->id, + 'name' => 'Участие в сделке (сторона 1)', + 'schedule_date' => $validatedData['deal_date'] ?? now(), + 'comment' => 'Создание сделки: ' . $validatedData['title'], + 'create_date' => now(), + ]; + } + } + + // Получаем ID клиентов стороны 2 + $sideTwoClientIds = []; + if (!empty($validatedData['side_two_clients'])) { + foreach ($validatedData['side_two_clients'] as $client) { + if (is_array($client) && isset($client['id'])) { + $sideTwoClientIds[] = $client['id']; + } elseif (is_numeric($client)) { + $sideTwoClientIds[] = (int) $client; + } + } + } + + // Добавляем клиента из заявки, если его нет в списке + $sideTwoClientId = $validatedData['side_two_requisition_id'] + ? Requisition::where('id', $validatedData['side_two_requisition_id'])->value('client_id') + : null; + + if ($sideTwoClientId && !in_array($sideTwoClientId, $sideTwoClientIds)) { + $sideTwoClientIds[] = $sideTwoClientId; + } + + // Получаем who_work для всех клиентов стороны 2 + $sideTwoClientWhoWork = []; + if (!empty($sideTwoClientIds)) { + $sideTwoClientWhoWork = Client::whereIn('id', $sideTwoClientIds) + ->pluck('who_work', 'id') + ->toArray(); + } + + // Создаем события для клиентов стороны 2 + if (!empty($validatedData['side_two_requisition_id']) && !empty($sideTwoClientIds)) { + $defaultUserId = Requisition::where('id', $validatedData['side_two_requisition_id'])->value('who_work') ?: auth()->id(); + + foreach ($sideTwoClientIds as $clientId) { + $userId = $sideTwoClientWhoWork[$clientId] ?? $defaultUserId; + + if (empty($userId)) { + $userId = auth()->id(); + } + + $clientEvents[] = [ + 'user_id' => $userId, + 'client_id' => $clientId, + 'req_id' => 0, + 'type' => 'deal', + 'deal_id' => $deal->id, + 'name' => 'Участие в сделке (сторона 2)', + 'schedule_date' => $validatedData['deal_date'] ?? now(), + 'comment' => 'Создание сделки: ' . $validatedData['title'], + 'create_date' => now(), + ]; + } + } + + if (!empty($clientEvents)) { + UserClientEvent::insert($clientEvents); + } + + // События для заявок + $reqEvents = []; + + if (!empty($validatedData['side_one_requisition_id'])) { + $sideOneReqUserId = Requisition::where('id', $validatedData['side_one_requisition_id'])->value('who_work') ?: auth()->id(); + $reqEvents[] = [ + 'user_id' => $sideOneReqUserId, + 'client_id' => 0, + 'req_id' => $validatedData['side_one_requisition_id'], + 'type' => 'deal', + 'deal_id' => $deal->id, + 'name' => 'Привязана к сделке (сторона 1)', + 'schedule_date' => $validatedData['deal_date'] ?? now(), + 'comment' => 'Создание сделки: ' . $validatedData['title'], + 'create_date' => now(), + ]; + } + + if (!empty($validatedData['side_two_requisition_id'])) { + $sideTwoReqUserId = Requisition::where('id', $validatedData['side_two_requisition_id'])->value('who_work') ?: auth()->id(); + $reqEvents[] = [ + 'user_id' => $sideTwoReqUserId, + 'client_id' => 0, + 'req_id' => $validatedData['side_two_requisition_id'], + 'type' => 'deal', + 'deal_id' => $deal->id, + 'name' => 'Привязана к сделке (сторона 2)', + 'schedule_date' => $validatedData['deal_date'] ?? now(), + 'comment' => 'Создание сделки: ' . $validatedData['title'], + 'create_date' => now(), + ]; + } + + if (!empty($reqEvents)) { + UserClientEvent::insert($reqEvents); + } + + $objectIds = array_unique(array_filter([ + $validatedData['side_one_object_id'] ?? null, + $validatedData['side_two_object_id'] ?? null, + ])); + + if (!empty($objectIds)) { + $objectEvents = []; + foreach ($objectIds as $objectId) { + $objectEvents[] = [ + 'user_id' => auth()->id(), + 'object_id' => $objectId, + 'type' => 'deal', + 'deal_id' => $deal->id, + 'schedule_date' => $validatedData['deal_date'] ?? now(), + 'sum' => $validatedData['contract_price'] ?? null, + 'comment' => 'Создание сделки: ' . $validatedData['title'], + 'create_date' => now(), + ]; + } + UserObjectEvent::insert($objectEvents); + } + + return response()->json([ + 'success' => true, + 'deal' => $deal, + 'message' => 'Сделка успешно создана', + ], 201); + } + + /** + * Получить сделку по ID + */ + public function show($id) + { + $deal = Deal::with([ + 'innerDocs.creator' => function($query) { + $query->select('id', 'last_name', 'first_name'); + }, + 'innerDocs.deals' => function($query) { + $query->select([ + 'deals.id', + 'deal_inner_doc.inner_doc_id', + 'deal_inner_doc.side_one', + 'deal_inner_doc.side_two', + 'deal_inner_doc.comment' + ]); + } + ])->findOrFail($id); + + $deal->innerDocs->each(function ($doc) { + collect($doc->deals)->each(function ($deal) { + $this->enrichSideWithClientData($deal, 'side_one'); + $this->enrichSideWithClientData($deal, 'side_two'); + }); + }); + + return response()->json([ + 'success' => true, + 'data' => $deal, + ]); + } + + protected function enrichSideWithClientData($deal, $sideField) + { + if (empty($deal->pivot->{$sideField})) { + $deal->{$sideField} = []; + return; + } + + $sideData = json_decode($deal->pivot->{$sideField}, true); + $clientIds = array_column($sideData, 'client_id'); + + $clients = Client::whereIn('id', $clientIds) + ->select('id', 'fio', 'phone') + ->get() + ->keyBy('id'); + + $enrichedSide = array_map(function ($item) use ($clients) { + $client = $clients[$item['client_id'] ?? null]; + return [ + 'role' => $item['role'], + 'client_id' => $item['client_id'], + 'fio' => $client->fio ?? 'Неизвестный клиент', + 'phone' => $client->phone ?? null + ]; + }, $sideData); + + $deal->{$sideField} = $enrichedSide; + } + + /** + * Обновить сделку + */ + public function update(Request $request, $id) + { + $validatedData = $request->validate([ + 'title' => 'sometimes|string|max:255', + 'side_one' => 'sometimes|string', + 'side_two' => 'sometimes|string', + 'side_one_requisition_id' => 'nullable|integer|exists:requisitions,id', + 'side_two_requisition_id' => 'nullable|integer|exists:requisitions,id', + 'side_one_object_id' => 'nullable|integer', + 'side_two_object_id' => 'nullable|integer', + 'side_one_clients' => 'nullable|array', + 'side_two_clients' => 'nullable|array', + 'contract_price' => 'nullable|numeric', + 'object_price' => 'nullable|numeric', + 'commission_buyer' => 'nullable|numeric', + 'commission_seller' => 'nullable|numeric', + 'agent_commission' => 'nullable|numeric', + 'total_commission' => 'nullable|numeric', + 'sellers_count' => 'nullable|integer', + 'buyers_count' => 'nullable|integer', + 'proxy_sale' => 'nullable|boolean', + 'e_registration' => 'nullable|boolean', + 'mortgaged' => 'nullable|boolean', + 'mortgagee' => 'nullable|string', + 'debt_amount' => 'nullable|numeric', + 'has_counter_agent' => 'nullable|boolean', + 'counter_agent_name' => 'nullable|string', + 'counter_agent_commission' => 'nullable|numeric', + 'counter_agent_phone' => 'nullable|string', + 'deal_date' => 'nullable|date', + 'employees' => 'nullable|array', + 'employees.*' => 'integer|exists:users,id', + 'documents' => 'nullable|array', + 'documents.*' => 'integer', + ]); + + $deal = Deal::findOrFail($id); + + $currentReqIds = array_filter([ + $deal->side_one_requisition_id, + $deal->side_two_requisition_id, + ]); + + $this->validateRequisitionsNotInDeal($validatedData['side_one_requisition_id'] ?? null, $currentReqIds); + $this->validateRequisitionsNotInOtherDeal( + $validatedData['side_two_requisition_id'] ?? null, + $id + ); + + $deal->update($validatedData); + + if (!empty($validatedData['employees'])) { + $deal->users()->sync($validatedData['employees']); + } + + if (isset($validatedData['documents'])) { + $existingDocIds = Document::active() + ->where('target_id', $id) + ->where('target_type', 10) + ->pluck('id') + ->toArray(); + + $removedDocIds = array_diff($existingDocIds, $validatedData['documents']); + if (!empty($removedDocIds)) { + Document::whereIn('id', $removedDocIds)->each->softDelete(); + } + + $addedDocIds = array_diff($validatedData['documents'], $existingDocIds); + if (!empty($addedDocIds)) { + Document::whereIn('id', $addedDocIds) + ->where('target_id', 0) + ->update(['target_id' => $id]); + } + } + + if (!empty($deal->side_one_requisition_id)) { + Requisition::where('id', $deal->side_one_requisition_id) + ->update(['deal_id' => $id]); + + if (isset($validatedData['employees'])) { + $sideOneReq = Requisition::find($deal->side_one_requisition_id); + if ($sideOneReq) { + $whoWork = (int) $sideOneReq->who_work; + $currentUserId = auth()->id(); + + $doers = is_string($sideOneReq->doers) ? json_decode($sideOneReq->doers, true) : ($sideOneReq->doers ?? []); + if (!is_array($doers)) $doers = []; + + $newDoers = $doers; + $changed = false; + + foreach ($validatedData['employees'] as $employeeId) { + $employeeId = (int) $employeeId; + + if ($employeeId === $whoWork || $employeeId === $currentUserId) { + continue; + } + + if (array_key_exists($employeeId, $doers)) { + continue; + } + + $exists = EventClient::where('req_id', $deal->side_one_requisition_id) + ->where('from_id', $employeeId) + ->where('event', 'transmitted_parallel') + ->exists(); + + if (!$exists) { + EventClient::create([ + 'user_id' => $whoWork ?: $currentUserId, + 'req_id' => $deal->side_one_requisition_id, + 'from_id' => $employeeId, + 'client_id' => $sideOneReq->client_id, + 'event' => 'transmitted_parallel', + 'read' => 0, + 'send_telegramm' => 1, + ]); + + $newDoers[$employeeId] = 0; + $changed = true; + + DB::table('requisition_doers')->insert([ + 'requisition_id' => $deal->side_one_requisition_id, + 'user_id' => $employeeId, + 'confirm' => 0, + ]); + } + } + + if ($changed) { + $sideOneReq->update([ + 'doers' => json_encode($newDoers), + 'doers_confirm' => 0, + ]); + } + } + } + } + + if (!empty($deal->side_two_requisition_id)) { + Requisition::where('id', $deal->side_two_requisition_id) + ->update(['deal_id' => $id]); + } + + // Получаем ID клиентов стороны 1 + $sideOneClientIds = []; + if (!empty($validatedData['side_one_clients'])) { + foreach ($validatedData['side_one_clients'] as $client) { + if (is_array($client) && isset($client['id'])) { + $sideOneClientIds[] = $client['id']; + } elseif (is_numeric($client)) { + $sideOneClientIds[] = (int) $client; + } + } + } + + $sideOneClientId = $deal->side_one_requisition_id + ? Requisition::where('id', $deal->side_one_requisition_id)->value('client_id') + : null; + + if ($sideOneClientId && !in_array($sideOneClientId, $sideOneClientIds)) { + $sideOneClientIds[] = $sideOneClientId; + } + + // Получаем who_work для всех клиентов стороны 1 + $sideOneClientWhoWork = []; + if (!empty($sideOneClientIds)) { + $sideOneClientWhoWork = Client::whereIn('id', $sideOneClientIds) + ->pluck('who_work', 'id') + ->toArray(); + } + + // Обновляем события для клиентов стороны 1 + if (!empty($deal->side_one_requisition_id)) { + $defaultUserId = Requisition::where('id', $deal->side_one_requisition_id)->value('who_work') ?: auth()->id(); + + // Сначала удаляем старые события для этой стороны, чтобы избежать дублирования + UserClientEvent::where('deal_id', $id) + ->where('type', 'deal') + ->where('name', 'like', '%сторона 1%') + ->delete(); + + foreach ($sideOneClientIds as $clientId) { + $userId = $sideOneClientWhoWork[$clientId] ?? $defaultUserId; + + if (empty($userId)) { + $userId = auth()->id(); + } + + UserClientEvent::create([ + 'user_id' => $userId, + 'client_id' => $clientId, + 'req_id' => 0, + 'type' => 'deal', + 'deal_id' => $id, + 'name' => 'Участие в сделке (сторона 1)', + 'schedule_date' => $deal->deal_date, + 'comment' => 'Обновление сделки: ' . $deal->title, + 'create_date' => now(), + ]); + } + } + + // Получаем ID клиентов стороны 2 + $sideTwoClientIds = []; + if (!empty($validatedData['side_two_clients'])) { + foreach ($validatedData['side_two_clients'] as $client) { + if (is_array($client) && isset($client['id'])) { + $sideTwoClientIds[] = $client['id']; + } elseif (is_numeric($client)) { + $sideTwoClientIds[] = (int) $client; + } + } + } + + $sideTwoClientId = $deal->side_two_requisition_id + ? Requisition::where('id', $deal->side_two_requisition_id)->value('client_id') + : null; + + if ($sideTwoClientId && !in_array($sideTwoClientId, $sideTwoClientIds)) { + $sideTwoClientIds[] = $sideTwoClientId; + } + + // Получаем who_work для всех клиентов стороны 2 + $sideTwoClientWhoWork = []; + if (!empty($sideTwoClientIds)) { + $sideTwoClientWhoWork = Client::whereIn('id', $sideTwoClientIds) + ->pluck('who_work', 'id') + ->toArray(); + } + + // Обновляем события для клиентов стороны 2 + if (!empty($deal->side_two_requisition_id)) { + $defaultUserId = Requisition::where('id', $deal->side_two_requisition_id)->value('who_work') ?: auth()->id(); + + // Сначала удаляем старые события для этой стороны + UserClientEvent::where('deal_id', $id) + ->where('type', 'deal') + ->where('name', 'like', '%сторона 2%') + ->delete(); + + foreach ($sideTwoClientIds as $clientId) { + $userId = $sideTwoClientWhoWork[$clientId] ?? $defaultUserId; + + UserClientEvent::create([ + 'user_id' => $userId, + 'client_id' => $clientId, + 'req_id' => 0, + 'type' => 'deal', + 'deal_id' => $id, + 'name' => 'Участие в сделке (сторона 2)', + 'schedule_date' => $deal->deal_date, + 'comment' => 'Обновление сделки: ' . $deal->title, + 'create_date' => now(), + ]); + } + } + + // Обновляем события для заявок + foreach ([$deal->side_one_requisition_id, $deal->side_two_requisition_id] as $reqId) { + if (empty($reqId)) continue; + + // Удаляем старые события для этой заявки + UserClientEvent::where('deal_id', $id) + ->where('req_id', $reqId) + ->where('type', 'deal') + ->delete(); + + $reqUserId = Requisition::where('id', $reqId)->value('who_work') ?: auth()->id(); + + UserClientEvent::create([ + 'user_id' => $reqUserId, + 'client_id' => 0, + 'req_id' => $reqId, + 'type' => 'deal', + 'deal_id' => $id, + 'name' => 'Связана со сделкой', + 'schedule_date' => $deal->deal_date, + 'comment' => 'Обновление сделки: ' . $deal->title, + 'create_date' => now(), + ]); + } + + // Обновляем события для объектов + $objectIds = array_unique(array_filter([ + $deal->side_one_object_id ?? null, + $deal->side_two_object_id ?? null, + ])); + + // Удаляем старые события для объектов + UserObjectEvent::where('deal_id', $id) + ->where('type', 'deal') + ->delete(); + + foreach ($objectIds as $objectId) { + UserObjectEvent::create([ + 'user_id' => auth()->id(), + 'object_id' => $objectId, + 'type' => 'deal', + 'deal_id' => $id, + 'schedule_date' => $deal->deal_date, + 'sum' => $deal->contract_price, + 'comment' => 'Обновление сделки: ' . $deal->title, + 'create_date' => now(), + ]); + } + + return response()->json([ + 'success' => true, + 'data' => $deal, + 'message' => 'Сделка успешно обновлена', + ], 200); + } + + /** + * Удалить сделку + */ + public function destroy($id) + { + Deal::destroy($id); + return response()->json(null, 204); + } + + public function assignEmployees(Request $request, $dealId) + { + $deal = Deal::find($dealId); + + if ($deal) { + $employeeIds = $request->input('employee_ids'); + $deal->users()->sync($employeeIds); + return response()->json(['message' => 'Сотрудники успешно назначены']); + } + + return response()->json(['message' => 'Сделка не найдена'], 404); + } + + public function getDealEmployees($dealId) + { + $deal = Deal::find($dealId); + + if (!$deal) { + return response()->json(['message' => 'Сделка не найдена'], 404); + } + + $employees = $deal->users() + ->select([ + 'users.id', + 'users.fio', + 'users.first_name', + 'users.last_name', + 'users.middle_name', + 'users.phone', + 'users.role_id', + 'users.user_logo' + ]) + ->with('role:id,name') + ->where('blocked', 0) + ->get(); + + return response()->json($employees); + } + + public function storeDocument(Request $request) + { + $data = $request->validate([ + 'template_data' => 'required|array', + 'template_data.blank_id' => 'required|integer|exists:inner_blanks,id', + 'template_data.object_id' => 'nullable|integer|exists:objects,id', + 'template_data.sobstv' => 'nullable|array', + 'template_data.clients' => 'nullable|array', + 'deal_id' => 'nullable|integer|exists:deals,id', + 'side_one' => 'nullable|array', + 'side_two' => 'nullable|array', + 'comment' => 'nullable|string|max:500' + ]); + + try { + $clientIds = $data['template_data']['clients'] ?? []; + $sobstvIds = $data['template_data']['sobstv'] ?? []; + + $mainClientId = !empty($clientIds) ? $clientIds[0] : null; + $mainSobstvId = !empty($sobstvIds) ? $sobstvIds[0] : null; + + $result = app(DocumentService::class)->fillFromTemplate( + $data['template_data']['blank_id'], + $mainClientId, + $data['template_data']['object_id'] ?? null, + $clientIds, + $sobstvIds + ); + + $dealDoc = DealInnerDoc::create([ + 'deal_id' => $data['deal_id'] ?? null, + 'inner_doc_id' => $result['doc_id'], + 'side_one' => $data['side_one'] ?? null, + 'side_two' => $data['side_two'] ?? null, + 'comment' => $data['comment'] ?? null + ]); + + $innerDoc = InnerDoc::with([ + 'creator:id,last_name,first_name', + 'deals' => function ($query) { + $query->select( + 'deals.id', + 'deal_inner_doc.side_one', + 'deal_inner_doc.side_two', + 'deal_inner_doc.comment' + ); + } + ])->findOrFail($result['doc_id']); + + foreach ($innerDoc->deals as $deal) { + $this->enrichSideWithClientData($deal, 'side_one'); + $this->enrichSideWithClientData($deal, 'side_two'); + } + + return response()->json([ + 'success' => true, + 'data' => $innerDoc + ], 201); + + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => $e->getMessage() + ], 500); + } + } + + protected function validateRequisitionsNotInDeal($requisitionId, $excludeIds = []) + { + if (empty($requisitionId)) { + return; + } + + if (in_array($requisitionId, $excludeIds)) { + return; + } + + $requisition = Requisition::find($requisitionId); + + if ($requisition && !is_null($requisition->deal_id)) { + throw \Illuminate\Validation\ValidationException::withMessages([ + 'requisition_id' => "Заявка #{$requisitionId} уже привязана к сделке #{$requisition->deal_id}" + ]); + } + } + + protected function validateRequisitionsNotInOtherDeal($requisitionId, $currentDealId) + { + if (empty($requisitionId)) { + return; + } + + $requisition = Requisition::find($requisitionId); + + if ($requisition && !is_null($requisition->deal_id) && $requisition->deal_id != $currentDealId) { + throw \Illuminate\Validation\ValidationException::withMessages([ + 'requisition_id' => "Заявка #{$requisitionId} уже привязана к другой сделке #{$requisition->deal_id}" + ]); + } + } + +} diff --git a/engine/classes/BiAnalyticsLogger.php b/engine/classes/BiAnalyticsLogger.php new file mode 100644 index 0000000..f966d47 --- /dev/null +++ b/engine/classes/BiAnalyticsLogger.php @@ -0,0 +1,162 @@ +db = $db; + $this->companyId = (int)$companyId; + } + + /** + * Создать событие в outbox + * + * @param string $entityType Тип сущности: 'client' или 'requisition' + * @param int $entityId ID сущности + * @param string $eventType Тип события + * @param int $actorUserId ID пользователя + * @param array $before Данные до изменения + * @param array $after Данные после изменения + * + * @return string UUID созданного события + * @throws Exception + */ + public function logEvent($entityType, $entityId, $eventType, $actorUserId, $before = array(), $after = array()) { + + // Валидация + if (!in_array($entityType, array('client', 'requisition'))) { + throw new Exception("Invalid entity type: " . $entityType); + } + + $entityId = (int)$entityId; + $actorUserId = (int)$actorUserId; + + // Определяем изменённые поля + $changedFields = array_keys(array_diff_assoc($after, $before)); + + // Формируем payload + $payload = array( + 'schema_version' => 1, + 'entity' => array( + 'type' => $entityType, + 'id' => $entityId, + 'company_id' => $this->companyId + ), + 'occurred_at' => date('c'), + 'actor_user_id' => $actorUserId, + 'event' => array( + 'type' => $eventType, + 'changed_fields' => $changedFields + ), + 'before' => $before, + 'after' => $after + ); + + // Генерируем UUID v4 + $eventUuid = $this->generateUuid(); + + // Вставляем в outbox с prepared statement + $payloadJson = json_encode($payload, JSON_UNESCAPED_UNICODE); + + // Экранируем строковые значения для безопасной вставки + $escapedEventUuid = $this->db->quote($eventUuid); + $escapedCompanyId = (int)$this->companyId; // целое число не требует экранирования + $escapedEntityType = $this->db->quote($entityType); + $escapedEntityId = (int)$entityId; + $escapedEventType = $this->db->quote($eventType); + $escapedActorUserId = (int)$actorUserId; + $escapedPayloadJson = $this->db->quote($payloadJson); + + $sql = "INSERT INTO bi_analytics_events_outbox + (event_uuid, company_id, entity_type, entity_id, event_type, occurred_at, actor_user_id, payload_json) + VALUES ($escapedEventUuid, $escapedCompanyId, $escapedEntityType, $escapedEntityId, $escapedEventType, NOW(3), $escapedActorUserId, $escapedPayloadJson)"; + + try { + $this->db->query($sql); + return $eventUuid; + } catch (Exception $e) { + throw new Exception("Failed to insert event: " . $e->getMessage()); + } + } + + /** + * Создать событие: клиент создан + */ + public function logClientCreated($clientId, $actorUserId, $clientData) { + return $this->logEvent('client', $clientId, 'client.created', $actorUserId, array(), $clientData); + } + + /** + * Создать событие: клиент обновлён + */ + public function logClientUpdated($clientId, $actorUserId, $before, $after) { + return $this->logEvent('client', $clientId, 'client.updated', $actorUserId, $before, $after); + } + + /** + * Создать событие: заявка создана + */ + public function logRequisitionCreated($requisitionId, $actorUserId, $requisitionData) { + return $this->logEvent('requisition', $requisitionId, 'requisition.created', $actorUserId, array(), $requisitionData); + } + + /** + * Создать событие: заявка обновлена + */ + public function logRequisitionUpdated($requisitionId, $actorUserId, $before, $after) { + return $this->logEvent('requisition', $requisitionId, 'requisition.updated', $actorUserId, $before, $after); + } + + /** + * Создать событие: заявка перешла на этап + */ + public function logRequisitionMovedToStep($requisitionId, $actorUserId, $fromStepId, $toStepId) { + $before = array('step_id' => $fromStepId); + $after = array('step_id' => $toStepId); + return $this->logEvent('requisition', $requisitionId, 'requisition.moved_to_step', $actorUserId, $before, $after); + } + + /** + * Создать событие: смена ответственного + */ + public function logResponsibleChanged($entityType, $entityId, $actorUserId, $fromUserId, $toUserId) { + $before = array('who_work' => $fromUserId); + $after = array('who_work' => $toUserId); + return $this->logEvent($entityType, $entityId, 'responsible_changed', $actorUserId, $before, $after); + } + + /** + * Сгенерировать UUID v4 + * + * @return string + */ + private function generateUuid() { + return sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x', + mt_rand(0, 0xffff), mt_rand(0, 0xffff), + mt_rand(0, 0xffff), + mt_rand(0, 0xffff) & 0x0fff | 0x4000, + mt_rand(0, 0x3fff) | 0x8000, + mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff) + ); + } +} \ No newline at end of file