diff --git a/app/app/Http/Controllers/Api/SupportRequestController.php b/app/app/Http/Controllers/Api/SupportRequestController.php new file mode 100644 index 00000000..fe1d7598 --- /dev/null +++ b/app/app/Http/Controllers/Api/SupportRequestController.php @@ -0,0 +1,55 @@ +validate([ + 'name' => 'required|string|max:255', + 'contact' => 'required|string|max:255', + 'message' => 'required|string|max:5000', + ]); + + /** @var User $user */ + $user = $request->user(); + + $supportRequest = DB::transaction(function () use ($user, $validated): SupportRequest { + DB::statement('SET LOCAL app.current_tenant_id = '.(int) $user->tenant_id); + + return SupportRequest::create([ + 'tenant_id' => $user->tenant_id, + 'user_id' => $user->id, + 'name' => $validated['name'], + 'contact' => $validated['contact'], + 'message' => $validated['message'], + ]); + }); + + // Письмо — best-effort: заявка уже в БД, сбой почты не теряет её и не валит запрос. + try { + Mail::to(config('services.support.email'))->queue(new SupportRequestMail($supportRequest)); + } catch (\Throwable $e) { + Log::warning('SupportRequestMail queue failed', ['id' => $supportRequest->id, 'error' => $e->getMessage()]); + } + + return response()->json(['ok' => true], 201); + } +} diff --git a/app/routes/web.php b/app/routes/web.php index 806711ff..b4965d85 100644 --- a/app/routes/web.php +++ b/app/routes/web.php @@ -230,6 +230,9 @@ Route::middleware(['auth:sanctum', 'tenant'])->group(function () { Route::get('/api/dashboard/summary', 'App\Http\Controllers\Api\DashboardController@summary'); }); +// G7-A: клиентские заявки в техподдержку. +Route::middleware(['auth:sanctum', 'tenant'])->post('/api/support-requests', 'App\Http\Controllers\Api\SupportRequestController@store'); + // Сделки — single-resource CRUD + bulk + export. J1 (Sprint 3F, audit): // auth:sanctum + tenant. tenant_id берётся из auth()->user()->tenant_id // (SetTenantContext), НЕ из параметра запроса — закрывает кросс-tenant утечку. diff --git a/app/tests/Feature/Support/SupportRequestControllerTest.php b/app/tests/Feature/Support/SupportRequestControllerTest.php new file mode 100644 index 00000000..01db4397 --- /dev/null +++ b/app/tests/Feature/Support/SupportRequestControllerTest.php @@ -0,0 +1,57 @@ +tenant = Tenant::factory()->create(); + $this->user = User::factory()->create(['tenant_id' => $this->tenant->id]); +}); + +it('создаёт заявку в БД и ставит письмо в очередь', function () { + Mail::fake(); + actingAs($this->user); + + postJson('/api/support-requests', [ + 'name' => 'Иван', + 'contact' => '+79991234567', + 'message' => 'Не приходят лиды.', + ])->assertCreated()->assertJson(['ok' => true]); + + $row = SupportRequest::query()->where('tenant_id', $this->tenant->id)->first(); + expect($row)->not->toBeNull() + ->and($row->user_id)->toBe($this->user->id) + ->and($row->name)->toBe('Иван') + ->and($row->contact)->toBe('+79991234567') + ->and($row->message)->toBe('Не приходят лиды.'); + + Mail::assertQueued(SupportRequestMail::class, fn (SupportRequestMail $m) => $m->hasTo(config('services.support.email'))); +}); + +it('валидирует обязательные поля (422)', function () { + Mail::fake(); + actingAs($this->user); + postJson('/api/support-requests', ['name' => '', 'contact' => '', 'message' => '']) + ->assertStatus(422) + ->assertJsonValidationErrors(['name', 'contact', 'message']); +}); + +it('не теряет заявку при сбое почты', function () { + Mail::shouldReceive('to')->andThrow(new RuntimeException('smtp down')); + + actingAs($this->user); + + postJson('/api/support-requests', [ + 'name' => 'Пётр', 'contact' => 'p@example.org', 'message' => 'Вопрос.', + ])->assertCreated(); + + expect(SupportRequest::query()->where('tenant_id', $this->tenant->id)->count())->toBe(1); +});