Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
31 KiB
G7-A — Клиентская «Помощь» — Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Дать клиенту экран «Помощь» с email техподдержки + формой-заявкой (→ запись в БД support_requests + письмо в поддержку) + заготовкой JivoSite (config-driven).
Architecture: Backend — конфиг (services.php) + tenant-RLS таблица support_requests (schema.sql + guard'нутая миграция) + POST /api/support-requests (store в БД, затем Mail::queue в try/catch). Frontend — роут /help + пункт «Помощь» в сайдбаре + HelpView.vue (форма). JivoSite — условный <script> в shell-blade по env-ID. Спека: docs/superpowers/specs/2026-06-19-g7a-client-support-design.md.
Tech Stack: Laravel 13 / Pest 4 / PostgreSQL 16 (RLS, partitioning conventions); Vue 3 + Vuetify 3 / vue-tsc / eslint / Vite. Unisender Go (SMTP, очередь).
Окружение (факты):
- Pest:
composer --working-dir=app test -- <path>. Pint:composer --working-dir=app pint <files>. - Миграции: dev
php app/artisan migrate --force; тестDB_DATABASE=liderra_testing php app/artisan migrate --force. Полная пересборка теста:DB_DATABASE=liderra_testing php app/artisan migrate:fresh --force. - Фронт:
npm --prefix app run type-check/npm --prefix app run lint:vue/npm --prefix app run build. vitest сломан (G8) → фронт-юнитов нет, верификация = type-check+eslint+build+живой Playwright. - Коммит:
LEFTHOOK=0 git commit -m "..." -m "Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>". Push:git push gitea main. - ⚠ Репозиторий имеет ПРЕД-существующие грязные файлы (CLAUDE.md, .claude/settings.json, observer, CTemp*.txt) — НЕ трогать/не стейджить. Стейджить ТОЛЬКО явные пути, НИКОГДА
git add -A. - Схема — источник истины: новые таблицы в
db/schema.sqlИ в guard'нутую миграцию; после —migrate:fresh(test) зелёный + ZERO_DRIFT (0 таблиц БД отсутствуют в schema.sql).
Файловая карта
Backend (create): app/app/Models/SupportRequest.php, app/app/Http/Controllers/Api/SupportRequestController.php, app/app/Mail/SupportRequestMail.php, app/resources/views/emails/support_request.blade.php, app/database/migrations/2026_06_19_140000_create_support_requests.php, app/tests/Feature/Support/SupportRequestControllerTest.php.
Backend (modify): app/config/services.php, app/.env.example, app/routes/web.php, db/schema.sql, db/CHANGELOG_schema.md, app/resources/views/welcome.blade.php.
Frontend (create): app/resources/js/views/HelpView.vue, app/resources/js/api/support.ts.
Frontend (modify): app/resources/js/router/index.ts, app/resources/js/components/layout/AppSidebar.vue.
Task 1: Backend config (support email + JivoSite)
Files: Modify app/config/services.php, app/.env.example.
- Step 1: Add config keys
В app/config/services.php перед закрывающим ]; добавить:
// G7-A: клиентская «Помощь».
'support' => [
'email' => env('SUPPORT_EMAIL', 'support@liderra.app'),
],
'jivosite' => [
'widget_id' => env('JIVO_WIDGET_ID'),
],
- Step 2: .env.example
В app/.env.example добавить (рядом с MAIL_*):
SUPPORT_EMAIL=support@liderra.app
JIVO_WIDGET_ID=
- Step 3: Verify config loads
Run: php app/artisan config:clear && php app/artisan tinker --execute="echo config('services.support.email').'|'.(config('services.jivosite.widget_id') ?? 'null');"
Expected: support@liderra.app|null.
- Step 4: Commit
git add app/config/services.php app/.env.example
LEFTHOOK=0 git commit -m "feat(G7-A): конфиг support.email + jivosite.widget_id" -m "Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
Task 2: Таблица support_requests (schema.sql + миграция)
Files: Create app/database/migrations/2026_06_19_140000_create_support_requests.php; Modify db/schema.sql, db/CHANGELOG_schema.md.
- Step 1: Migration (guarded, pgsql_supplier — паттерн tenant_requisites)
Create app/database/migrations/2026_06_19_140000_create_support_requests.php:
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
public function up(): void
{
$supplier = DB::connection('pgsql_supplier');
$supplier->statement(<<<'SQL'
CREATE TABLE IF NOT EXISTS support_requests (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
user_id BIGINT NOT NULL REFERENCES users(id),
name VARCHAR(255) NOT NULL,
contact VARCHAR(255) NOT NULL,
message TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
)
SQL);
$supplier->statement('CREATE INDEX IF NOT EXISTS idx_support_requests_tenant ON support_requests (tenant_id, created_at DESC)');
$supplier->statement('ALTER TABLE support_requests ENABLE ROW LEVEL SECURITY');
// PG не поддерживает CREATE POLICY IF NOT EXISTS → дроп-перед-создание (идемпотентность к schema.sql).
$supplier->statement('DROP POLICY IF EXISTS support_requests_tenant_isolation ON support_requests');
$supplier->statement(<<<'SQL'
CREATE POLICY support_requests_tenant_isolation ON support_requests
USING (tenant_id = current_setting('app.current_tenant_id', true)::bigint)
SQL);
foreach (['crm_app_user', 'crm_supplier_worker'] as $role) {
$supplier->statement(<<<SQL
DO \$\$
BEGIN
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '{$role}') THEN
GRANT SELECT, INSERT ON support_requests TO {$role};
GRANT USAGE, SELECT ON SEQUENCE support_requests_id_seq TO {$role};
END IF;
END
\$\$
SQL);
}
}
public function down(): void
{
DB::connection('pgsql_supplier')->statement('DROP TABLE IF EXISTS support_requests CASCADE');
}
};
- Step 2: Add the table to db/schema.sql
В db/schema.sql, в логичной секции (рядом с другими tenant-RLS клиентскими таблицами, напр. возле tenant_requisites), добавить:
CREATE TABLE support_requests (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
user_id BIGINT NOT NULL REFERENCES users(id),
name VARCHAR(255) NOT NULL,
contact VARCHAR(255) NOT NULL,
message TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_support_requests_tenant ON support_requests (tenant_id, created_at DESC);
ALTER TABLE support_requests ENABLE ROW LEVEL SECURITY;
CREATE POLICY support_requests_tenant_isolation ON support_requests
USING (tenant_id = current_setting('app.current_tenant_id', true)::bigint);
GRANT SELECT, INSERT ON support_requests TO crm_app_user, crm_supplier_worker;
GRANT USAGE, SELECT ON SEQUENCE support_requests_id_seq TO crm_app_user, crm_supplier_worker;
COMMENT ON TABLE support_requests IS 'Заявки клиента в техподдержку (G7-A). RLS по tenant_id; разбор вручную (письмо + журнал).';
Match the file's indentation/comment style. Place near other tenant-RLS tables.
- Step 3: Header metrics + version
В header db/schema.sql: tables +1 (regular), indexes +1, RLS policies +1. Бамп версии (читать текущую, напр. v8.47 → v8.48).
- Step 4: CHANGELOG_schema.md
Добавить top-запись: версия, дата 2026-06-19, «G7-A: таблица support_requests (заявки клиента в техподдержку) — RLS tenant_isolation, индекс, GRANTs. Миграция 2026_06_19_140000 (guarded).»
- Step 5: Apply on dev + test, verify migrate:fresh + ZERO_DRIFT
Run:
php app/artisan migrate --force
DB_DATABASE=liderra_testing php app/artisan migrate:fresh --force 2>&1 | tail -5
Expected: всё DONE, без «already exists». Затем ZERO_DRIFT:
DB_DATABASE=liderra_testing php app/artisan tinker --execute="\$db=collect(DB::select(\"SELECT tablename FROM pg_tables WHERE schemaname='public'\"))->pluck('tablename'); \$s=file_get_contents(dirname(base_path()).'/db/schema.sql'); \$m=[]; foreach(\$db as \$t){ if(preg_match('/_y20\\d\\d_m\\d\\d\$/',\$t))continue; if(in_array(\$t,['migrations','jobs','job_batches','failed_jobs','cache','cache_locks','sessions','password_reset_tokens']))continue; if(!preg_match('/CREATE TABLE '.\$t.'\\b/',\$s))\$m[]=\$t; } echo empty(\$m)?'ZERO_DRIFT':('STILL_MISSING: '.implode(',',\$m));"
Expected: ZERO_DRIFT. И grep -cE "CREATE TABLE support_requests\b" db/schema.sql → 1.
- Step 6: Commit
git add app/database/migrations/2026_06_19_140000_create_support_requests.php db/schema.sql db/CHANGELOG_schema.md
LEFTHOOK=0 git commit -m "feat(G7-A): таблица support_requests (schema + миграция, RLS)" -m "Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
Task 3: Модель SupportRequest
Files: Create app/app/Models/SupportRequest.php.
- Step 1: Model (явные @property — ide-helper:models может пропустить)
Create app/app/Models/SupportRequest.php:
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Carbon;
/**
* Заявка клиента в техподдержку (G7-A). RLS по tenant_id; created_at — DB default.
*
* @property int $id
* @property int $tenant_id
* @property int $user_id
* @property string $name
* @property string $contact
* @property string $message
* @property Carbon $created_at
*/
class SupportRequest extends Model
{
protected $table = 'support_requests';
public $timestamps = false; // только created_at (DB DEFAULT now()), updated_at нет
protected $fillable = [
'tenant_id', 'user_id', 'name', 'contact', 'message',
];
protected function casts(): array
{
return ['created_at' => 'datetime'];
}
}
- Step 2: Commit
composer --working-dir=app pint app/Models/SupportRequest.php
git add app/app/Models/SupportRequest.php
LEFTHOOK=0 git commit -m "feat(G7-A): модель SupportRequest" -m "Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
Task 4: Mailable + шаблон письма
Files: Create app/app/Mail/SupportRequestMail.php, app/resources/views/emails/support_request.blade.php.
- Step 1: Mailable (паттерн NewLeadsDigestMail)
Create app/app/Mail/SupportRequestMail.php:
<?php
declare(strict_types=1);
namespace App\Mail;
use App\Models\SupportRequest;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
/**
* Письмо в техподдержку о новой заявке клиента (G7-A). Адресат — config('services.support.email').
*/
class SupportRequestMail extends Mailable
{
use Queueable;
use SerializesModels;
public function __construct(public SupportRequest $request) {}
public function envelope(): Envelope
{
return new Envelope(
subject: 'Лидерра. Заявка в поддержку #'.$this->request->id,
);
}
public function content(): Content
{
return new Content(
view: 'emails.support_request',
with: ['r' => $this->request],
);
}
}
- Step 2: Email blade
Create app/resources/views/emails/support_request.blade.php:
<p>Новая заявка в техподдержку Лидерры.</p>
<ul>
<li><strong>Заявка #:</strong> {{ $r->id }}</li>
<li><strong>Тенант:</strong> {{ $r->tenant_id }}</li>
<li><strong>Пользователь:</strong> {{ $r->user_id }}</li>
<li><strong>Имя:</strong> {{ $r->name }}</li>
<li><strong>Контакт:</strong> {{ $r->contact }}</li>
<li><strong>Время:</strong> {{ $r->created_at?->format('d.m.Y H:i') }}</li>
</ul>
<p><strong>Сообщение:</strong></p>
<p>{{ $r->message }}</p>
- Step 3: Commit
composer --working-dir=app pint app/Mail/SupportRequestMail.php
git add app/app/Mail/SupportRequestMail.php app/resources/views/emails/support_request.blade.php
LEFTHOOK=0 git commit -m "feat(G7-A): SupportRequestMail + шаблон письма" -m "Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
Task 5: Контроллер + роут (TDD)
Files: Create app/app/Http/Controllers/Api/SupportRequestController.php, app/tests/Feature/Support/SupportRequestControllerTest.php; Modify app/routes/web.php.
- Step 1: Write failing test
Create app/tests/Feature/Support/SupportRequestControllerTest.php:
<?php
declare(strict_types=1);
use App\Mail\SupportRequestMail;
use App\Models\SupportRequest;
use App\Models\Tenant;
use App\Models\User;
use Illuminate\Support\Facades\Mail;
use function Pest\Laravel\actingAs;
use function Pest\Laravel\postJson;
beforeEach(function () {
Mail::fake();
$this->tenant = Tenant::factory()->create();
$this->user = User::factory()->create(['tenant_id' => $this->tenant->id]);
});
it('создаёт заявку в БД и ставит письмо в очередь', function () {
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 () {
actingAs($this->user);
postJson('/api/support-requests', ['name' => '', 'contact' => '', 'message' => ''])
->assertStatus(422)
->assertJsonValidationErrors(['name', 'contact', 'message']);
});
it('не теряет заявку при сбое почты', function () {
actingAs($this->user);
Mail::shouldReceive('queue')->andThrow(new RuntimeException('smtp down'));
postJson('/api/support-requests', [
'name' => 'Пётр', 'contact' => 'p@example.org', 'message' => 'Вопрос.',
])->assertCreated();
expect(SupportRequest::query()->where('tenant_id', $this->tenant->id)->count())->toBe(1);
});
- Step 2: Run — verify it fails (no route/controller)
Run: composer --working-dir=app test -- tests/Feature/Support/SupportRequestControllerTest.php
Expected: FAIL (404 / route not defined).
- Step 3: Controller
Create app/app/Http/Controllers/Api/SupportRequestController.php:
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Mail\SupportRequestMail;
use App\Models\SupportRequest;
use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
/**
* G7-A: приём клиентских заявок в техподдержку. Запись в БД — основной канал;
* письмо в поддержку — best-effort (сбой SMTP не валит запрос, паттерн G1 sendCode).
*/
class SupportRequestController extends Controller
{
public function store(Request $request): JsonResponse
{
$validated = $request->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);
}
}
- Step 4: Route
В app/routes/web.php добавить (рядом с прочими auth:sanctum+tenant API-группами):
// G7-A: клиентские заявки в техподдержку.
Route::middleware(['auth:sanctum', 'tenant'])->post('/api/support-requests', 'App\Http\Controllers\Api\SupportRequestController@store');
- Step 5: Run — verify pass
Run: composer --working-dir=app test -- tests/Feature/Support/SupportRequestControllerTest.php
Expected: 3 passed.
- Step 6: Commit
composer --working-dir=app pint app/Http/Controllers/Api/SupportRequestController.php
git add app/app/Http/Controllers/Api/SupportRequestController.php app/routes/web.php app/tests/Feature/Support/SupportRequestControllerTest.php
LEFTHOOK=0 git commit -m "feat(G7-A): POST /api/support-requests + тест (store+mail+валидация)" -m "Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
Task 6: SPA-shell (meta support-email + JivoSite)
Files: Modify app/resources/views/welcome.blade.php.
- Step 1: Inject meta + conditional JivoSite
Заменить <head>...</head> блок в app/resources/views/welcome.blade.php, добавив после <title>:
<meta name="support-email" content="{{ config('services.support.email') }}">
@vite(['resources/css/app.css', 'resources/js/app.ts'])
@if(config('services.jivosite.widget_id'))
<script src="https://code.jivo.ru/widget/{{ config('services.jivosite.widget_id') }}" async></script>
@endif
</head>
(итог: <title> → <meta support-email> → @vite → условный JivoSite → </head>). NB: SRI к JivoSite неприменим (динамический loader); контроль — CSP-allowlist code.jivo.ru при включении на проде (пункт деплоя, пока id пуст — скрипта нет).
- Step 2: Verify blade renders (id пуст → нет скрипта)
Run: php app/artisan view:clear && curl -s http://127.0.0.1:8000/login | grep -c "support-email" (если сервер запущен; иначе пропустить — проверится в Playwright).
Expected: 1 (meta есть), и нет code.jivo.ru (id пуст).
- Step 3: Commit
git add app/resources/views/welcome.blade.php
LEFTHOOK=0 git commit -m "feat(G7-A): meta support-email + условный JivoSite в shell-blade" -m "Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
Task 7: Фронт — роут /help + пункт «Помощь» в меню
Files: Modify app/resources/js/router/index.ts, app/resources/js/components/layout/AppSidebar.vue.
- Step 1: Route
В app/resources/js/router/index.ts добавить (рядом с прочими app-роутами, напр. после /settings):
{
path: '/help',
name: 'help',
component: () => import('../views/HelpView.vue'),
meta: {
layout: 'app',
title: 'Помощь',
requiresAuth: true,
transition: 'ld-route-fadeup',
devIndex: 33,
devLabel: 'Помощь',
},
},
(devIndex 33 — следующий свободный; если 33 занят, взять следующий незанятый.)
- Step 2: Nav item «Помощь»
В app/resources/js/components/layout/AppSidebar.vue, в navGroups, в группу 'Команда' добавить пункт после «Настройки»:
{
eyebrow: 'Команда',
items: [
{ title: 'Настройки', icon: 'mdi-cog-outline', to: '/settings' },
{ title: 'Помощь', icon: 'mdi-lifebuoy', to: '/help' },
],
},
- Step 3: type-check (упадёт — HelpView ещё нет; это ок до Task 8)
Пропустить прогон до Task 8 (HelpView создаётся там). Коммит — в конце Task 8.
Task 8: Фронт — HelpView + api/support.ts
Files: Create app/resources/js/views/HelpView.vue, app/resources/js/api/support.ts.
- Step 1: api/support.ts
Create app/resources/js/api/support.ts:
import { apiClient } from './client';
export interface SupportRequestPayload {
name: string;
contact: string;
message: string;
}
export async function submitSupportRequest(payload: SupportRequestPayload): Promise<void> {
await apiClient.post('/api/support-requests', payload);
}
NB: проверить имя экспортируемого axios-инстанса в app/resources/js/api/client.ts (grep export); если он называется иначе (напр. api / default), импортировать соответствующе.
- Step 2: HelpView.vue
Create app/resources/js/views/HelpView.vue:
<script setup lang="ts">
import { ref } from 'vue';
import { useAuthStore } from '../stores/auth';
import { submitSupportRequest } from '../api/support';
const auth = useAuthStore();
const supportEmail =
document.querySelector('meta[name="support-email"]')?.getAttribute('content') ?? 'support@liderra.app';
const name = ref(
[auth.user?.first_name, auth.user?.last_name].filter(Boolean).join(' ') || '',
);
const contact = ref(auth.user?.email ?? '');
const message = ref('');
const loading = ref(false);
const sent = ref(false);
const errorMsg = ref('');
const fieldErrors = ref<Record<string, string[]>>({});
async function submit() {
errorMsg.value = '';
fieldErrors.value = {};
if (!name.value.trim() || !contact.value.trim() || !message.value.trim()) {
errorMsg.value = 'Заполните все поля.';
return;
}
loading.value = true;
try {
await submitSupportRequest({ name: name.value, contact: contact.value, message: message.value });
sent.value = true;
message.value = '';
} catch (e: unknown) {
const err = e as { response?: { status?: number; data?: { errors?: Record<string, string[]> } } };
if (err.response?.status === 422 && err.response.data?.errors) {
fieldErrors.value = err.response.data.errors;
} else {
errorMsg.value = 'Не удалось отправить. Попробуйте ещё раз или напишите на почту.';
}
} finally {
loading.value = false;
}
}
</script>
<template>
<div class="help-view pa-6" data-testid="help-view">
<h1 class="text-h5 mb-1">Помощь</h1>
<p class="text-body-2 text-medium-emphasis mb-6">
Напишите нам — ответим на ваш контакт. Можно по почте, через форму ниже или в чат справа.
</p>
<v-card variant="outlined" class="pa-5 mb-4" max-width="640">
<h3 class="text-subtitle-2 mb-2">Почта техподдержки</h3>
<a :href="`mailto:${supportEmail}`" class="text-primary" data-testid="support-email">{{ supportEmail }}</a>
</v-card>
<v-card variant="outlined" class="pa-5" max-width="640">
<h3 class="text-subtitle-2 mb-3">Оставить заявку</h3>
<v-alert v-if="sent" type="success" variant="tonal" class="mb-4" data-testid="support-sent">
Заявка отправлена. Мы свяжемся с вами по указанному контакту.
</v-alert>
<v-alert v-if="errorMsg" type="error" variant="tonal" class="mb-4">{{ errorMsg }}</v-alert>
<v-text-field
v-model="name"
label="Имя"
:error-messages="fieldErrors.name"
density="comfortable"
class="mb-2"
data-testid="support-name"
/>
<v-text-field
v-model="contact"
label="Контакт (телефон или email)"
:error-messages="fieldErrors.contact"
density="comfortable"
class="mb-2"
data-testid="support-contact"
/>
<v-textarea
v-model="message"
label="Сообщение"
:error-messages="fieldErrors.message"
rows="4"
density="comfortable"
class="mb-3"
data-testid="support-message"
/>
<v-btn color="primary" :loading="loading" data-testid="support-submit" @click="submit">Отправить</v-btn>
</v-card>
</div>
</template>
NB: проверить, что auth.user имеет поля first_name/last_name/email (grep по stores/auth.ts / типу User); если имена иные — подставить фактические.
- Step 3: type-check + eslint + build
Run: npm --prefix app run type-check && npm --prefix app run lint:vue && npm --prefix app run build
Expected: 0 ошибок, build OK.
- Step 4: Commit (роут+меню+экран вместе)
git add app/resources/js/router/index.ts app/resources/js/components/layout/AppSidebar.vue app/resources/js/views/HelpView.vue app/resources/js/api/support.ts
LEFTHOOK=0 git commit -m "feat(G7-A): экран «Помощь» (форма-заявка) + пункт меню + роут" -m "Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
Task 9: Верификация
Files: только прогон.
- Step 1: Backend Pest + adjacent
Run: composer --working-dir=app test -- tests/Feature/Support tests/Feature/Auth/NotificationPreferencesTest.php
Expected: всё PASS (новый тест 3/3 + смежный регресс).
- Step 2: stan + type-check + eslint + build
Run:
composer --working-dir=app stan
npm --prefix app run type-check
npm --prefix app run lint:vue
npm --prefix app run build
Expected: stan 0 (если новые ложноположительные Pest-методы в SupportRequestControllerTest — добавить в phpstan-baseline тем же паттерном, что прочие тесты; см. как в проекте baseline'ятся TestCall::postJson), type-check/eslint 0, build OK.
- Step 3: migrate:fresh + ZERO_DRIFT (повтор после всех правок)
Run: DB_DATABASE=liderra_testing php app/artisan migrate:fresh --force 2>&1 | tail -3 + ZERO_DRIFT-скан из Task 2 Step 5.
Expected: зелёный + ZERO_DRIFT.
- Step 4: Живой Playwright
Поднять сервер (php app/artisan serve), пересобрать ассеты (npm --prefix app run build), залогиниться демо-клиентом (admin@demo.local / password, либо создать через самозапись). Через Playwright MCP на 127.0.0.1:8000:
-
(а) в боковом меню есть пункт «Помощь»; клик →
/help. -
(б) экран «Помощь»: виден email техподдержки (
data-testid="support-email"), форма (имя/контакт/сообщение). -
(в) заполнить сообщение, нажать «Отправить» → зелёный алерт
data-testid="support-sent"; консоль без ошибок. -
(г) проверить запись в БД:
php app/artisan tinker --execute="echo App\Models\SupportRequest::count();"≥ 1. -
(д) JivoSite: id пуст → чата нет, экран рабочий (нет JS-ошибок про jivo).
-
Step 5: Финальный grep — нет хвостов
Run: grep -rin "support_requests\|SupportRequest" app/app | grep -vi "test" — ожидается: модель/контроллер/мейлабл/миграция (живые ссылки, без мусора).
Task 10: Push + память
- Step 1: Push
git push gitea main
Expected: pre-push (gitleaks + lychee) проходит.
- Step 2: Память
Обновить project-golive-findings-status.md: G7-A DONE (дата, что осталось — G7-B impersonation). Требует coverage: direct:memory-sync или фразу memory dump.
Self-Review плана (выполнено автором)
- Покрытие спеки: §2.1 конфиг → Task 1; §2.2 таблица → Task 2; §2.3 модель/эндпоинт/письмо → Task 3/4/5; §2.4 shell → Task 6; §3 фронт → Task 7/8; §4 поток → реализован Task 5+8; §5 ошибки → Task 5 (try/catch, 422) + Task 8 (UI-состояния); §6 тесты → Task 5 (TDD) + Task 9. Пробелов нет.
- Плейсхолдеры: нет TBD/«добавить валидацию» без кода — весь код приведён. NB-пометки (проверить имя axios-инстанса / поля User / devIndex) — это явные verify-инструкции, не пропуски.
- Согласованность имён: таблица
support_requests/ модельSupportRequest/ политикаsupport_requests_tenant_isolation/ эндпоинтPOST /api/support-requests/config('services.support.email')/config('services.jivosite.widget_id')/meta[name=support-email]/submitSupportRequest— единообразно во всех задачах.