This commit is contained in:
mac 2026-08-05 21:06:56 +03:00
parent cb1b619a8b
commit f5fe4e25a0
2 changed files with 1013 additions and 0 deletions

View File

@ -0,0 +1,851 @@
<?php
namespace App\Http\Controllers;
use App\Models\Client;
use App\Models\Deal;
use App\Models\DealInnerDoc;
use App\Models\Document;
use App\Models\EventClient;
use App\Models\InnerDoc;
use App\Models\Requisition;
use App\Models\UserClientEvent;
use App\Models\UserObjectEvent;
use App\Services\Documents\DocumentService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class DealController extends Controller
{
/**
* Получить список сделок
*/
public function index()
{
return Deal::all();
}
/**
* Создать новую сделку
*/
public function store(Request $request)
{
$validatedData = $request->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}"
]);
}
}
}

View File

@ -0,0 +1,162 @@
<?php
/**
* Класс для логирования событий в BI Analytics (outbox)
*
* Используется для отправки событий из JoyWork в ClickHouse
* через таблицу bi_analytics_events_outbox
*/
class BiAnalyticsLogger {
/**
* @var MysqlPdo Подключение к базе данных
*/
private $db;
/**
* @var int ID компании (agency_id)
*/
private $companyId;
/**
* Конструктор
*
* @param MysqlPdo $db Подключение к БД
* @param int $companyId ID компании
*/
public function __construct($db, $companyId) {
$this->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)
);
}
}