# Публичные юр-страницы + страница цен (под модерацию ЮKassa) — Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: superpowers:subagent-driven-development (recommended) или superpowers:executing-plans. Steps use checkbox (`- [ ]`) syntax. Кодовая фраза стены: «роутер-наставник».

**Goal:** Разместить на публичном (без логина) слое liderra.ru документы и информацию, которые требует модерация ЮKassa: оферта, политика конфиденциальности, политика возврата, реквизиты продавца + контакты, публичная страница цен (7-ступенчатая сетка).

**Architecture:** Новый публичный layout (`PublicLayout`, full-width, шапка-бренд + футер с реквизитами/контактами/ссылками). Юр-тексты — в отдельном content-модуле (`content/legalDocs.ts`), рендерятся существующим `LegalDocView`. Цены — публичный read-only эндпоинт `GET /api/public/pricing` (переиспользует `PricingTierRepository::activeAt`), фронт `PricingView` его показывает. Реквизиты — единый константный модуль `constants/legal.ts`. Ничего в revenue-ядре/биллинге не меняется.

**Tech Stack:** Laravel 13 / Pest 4 (бэкенд), Vue 3 + Vuetify 3 / TypeScript (фронт). Палитра v8 Forest. Фронт-верификация — vue-tsc + ESLint + живой Playwright (vitest в проекте сломан — см. память).

**Источники:** требования ЮKassa — [DRAFT §4](../specs/2026-06-24-public-legal-block-yookassa-moderation-DRAFT.md); решения владельца (контакты `info@liderra.ru` / `8 913 519-12-64`, адрес Новосибирская д.5 кв.97, возврат остатка за 3 раб. дня за вычетом комиссий, цены — полная сетка 7 ступеней, без упоминания РКН, вычитка — владелец). Реальный тариф (с прода 2026-06-24): 70/65/60/55/50/45/40 ₽ по ступеням 100/250/500/1000/2500/5000/∞.

---

## File Structure

**Бэкенд (публичный эндпоинт цен):**

- Create `app/app/Http/Controllers/Api/PublicPricingController.php` — отдаёт текущую сетку.
- Modify `app/routes/web.php` — публичный `GET /api/public/pricing` (без auth, рядом с публичными webhook-маршрутами).
- Test `app/tests/Feature/Public/PublicPricingTest.php` — Pest.

**Фронт:**

- Create `app/resources/js/constants/legal.ts` — реквизиты продавца + контакты (единый источник).
- Create `app/resources/js/content/legalDocs.ts` — структурированные тексты оферты/политики/возврата.
- Create `app/resources/js/layouts/PublicLayout.vue` — публичный full-width layout с футером-реквизитами.
- Create `app/resources/js/views/PricingView.vue` — страница цен.
- Create `app/resources/js/api/public.ts` — `fetchPublicPricing()`.
- Modify `app/resources/js/components/AppShell.vue` — ветка `public` → `PublicLayout`.
- Modify `app/resources/js/views/legal/LegalDocView.vue` — рендер полных текстов (offer/privacy/refund) из content-модуля.
- Modify `app/resources/js/router/index.ts` — legal-regex + `refund`, layout `public`; новый `/pricing`.
- Modify `app/resources/js/layouts/AuthLayout.vue` — в футер логина добавить ссылки «Возврат» и «Цены».

---

## Task 1: Публичный эндпоинт цен (бэкенд, TDD)

**Files:**

- Create: `app/app/Http/Controllers/Api/PublicPricingController.php`
- Modify: `app/routes/web.php` (публичная секция, рядом со строкой 323 `/api/webhook/payment`)
- Test: `app/tests/Feature/Public/PublicPricingTest.php`

- [ ] **Step 1: Failing test**

```php
<?php
declare(strict_types=1);

use App\Models\PricingTier;
use function Pest\Laravel\getJson;

it('returns current active pricing ladder without auth', function () {
    PricingTier::query()->delete();
    $tiers = [
        [1, 100, 7000], [2, 250, 6500], [3, 500, 6000], [4, 1000, 5500],
        [5, 2500, 5000], [6, 5000, 4500], [7, null, 4000],
    ];
    foreach ($tiers as [$no, $leads, $kop]) {
        PricingTier::create([
            'tier_no' => $no, 'leads_in_tier' => $leads,
            'price_per_lead_kopecks' => $kop, 'is_active' => true,
            'effective_from' => '2026-06-22',
        ]);
    }

    $res = getJson('/api/public/pricing');

    $res->assertOk();
    $res->assertJsonCount(7, 'tiers');
    $res->assertJsonPath('tiers.0.tier_no', 1);
    $res->assertJsonPath('tiers.0.price_rub', '70.00');
    $res->assertJsonPath('tiers.0.leads_in_tier', 100);
    $res->assertJsonPath('tiers.6.tier_no', 7);
    $res->assertJsonPath('tiers.6.leads_in_tier', null);
    $res->assertJsonPath('tiers.6.price_rub', '40.00');
});

it('prefers the latest effective ladder over older one', function () {
    PricingTier::query()->delete();
    PricingTier::create(['tier_no' => 1, 'leads_in_tier' => 100, 'price_per_lead_kopecks' => 50000, 'is_active' => true, 'effective_from' => '1970-01-01']);
    PricingTier::create(['tier_no' => 1, 'leads_in_tier' => 100, 'price_per_lead_kopecks' => 7000, 'is_active' => true, 'effective_from' => '2026-06-22']);

    getJson('/api/public/pricing')->assertJsonPath('tiers.0.price_rub', '70.00');
});
```

- [ ] **Step 2: Run, verify it fails**

Run: `cd app && ./vendor/bin/pest tests/Feature/Public/PublicPricingTest.php`
Expected: FAIL (route `/api/public/pricing` not defined → 404).

- [ ] **Step 3: Implement controller**

```php
<?php

declare(strict_types=1);

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Repositories\PricingTierRepository;
use Carbon\CarbonImmutable;
use Illuminate\Http\JsonResponse;

/**
 * Публичный (без auth) список текущей тарифной сетки для страницы цен.
 * Read-only, без ПДн. Переиспользует PricingTierRepository::activeAt.
 * Требование ЮKassa: цены должны быть публично доступны.
 */
class PublicPricingController extends Controller
{
    public function index(PricingTierRepository $repo): JsonResponse
    {
        $tiers = $repo->activeAt(CarbonImmutable::now())
            ->map(fn ($t) => [
                'tier_no' => (int) $t->tier_no,
                'leads_in_tier' => $t->leads_in_tier === null ? null : (int) $t->leads_in_tier,
                'price_rub' => number_format((int) $t->price_per_lead_kopecks / 100, 2, '.', ''),
            ])
            ->values();

        return response()->json(['tiers' => $tiers]);
    }
}
```

- [ ] **Step 4: Add route**

В `app/routes/web.php` после блока публичного платёжного webhook (после строки `Route::post('/api/webhook/payment', ...)`):

```php
// Публичная (без auth) тарифная сетка — для страницы цен и модерации ЮKassa.
Route::get('/api/public/pricing', 'App\Http\Controllers\Api\PublicPricingController@index');
```

- [ ] **Step 5: Run, verify pass**

Run: `cd app && ./vendor/bin/pest tests/Feature/Public/PublicPricingTest.php`
Expected: PASS (2 passed).

- [ ] **Step 6: Commit**

```bash
git add app/app/Http/Controllers/Api/PublicPricingController.php app/routes/web.php app/tests/Feature/Public/PublicPricingTest.php
git commit -m "feat(billing): публичный эндпоинт текущей тарифной сетки /api/public/pricing"
```

---

## Task 2: Константы реквизитов продавца (фронт)

**Files:**

- Create: `app/resources/js/constants/legal.ts`

- [ ] **Step 1: Создать модуль реквизитов**

```ts
/**
 * Реквизиты продавца (ИП) и контакты — единый публичный источник для
 * футера, юр-страниц и страницы цен. Требование ЮKassa: реквизиты + контакты
 * на сайте. Значения согласованы владельцем 2026-06-24.
 */
export const SELLER = {
    legalName: 'Индивидуальный предприниматель Кондратьев Платон Дмитриевич',
    shortName: 'ИП Кондратьев П.Д.',
    inn: '245210851872',
    ogrnip: '326246800098042',
    address: 'Россия, Красноярский край, г. Красноярск, ул. Новосибирская, д. 5, кв. 97',
    email: 'info@liderra.ru',
    phone: '+7 913 519-12-64',
} as const;

export const LEGAL_LINKS = [
    { to: '/legal/offer', label: 'Оферта' },
    { to: '/legal/privacy', label: 'Политика конфиденциальности' },
    { to: '/legal/refund', label: 'Политика возврата' },
    { to: '/pricing', label: 'Цены' },
] as const;
```

- [ ] **Step 2: Type-check**

Run: `cd app && npm run type-check`
Expected: без новых ошибок по `constants/legal.ts`.

- [ ] **Step 3: Commit**

```bash
git add app/resources/js/constants/legal.ts
git commit -m "feat(legal): константы реквизитов продавца и юр-ссылок"
```

---

## Task 3: Контент юр-документов (фронт)

**Files:**

- Create: `app/resources/js/content/legalDocs.ts`

> Тексты — рабочая редакция под вычитку владельца. Модель: «оплата за лиды по предоплате с внутреннего баланса». Возврат — по решению владельца (остаток за 3 раб. дня за вычетом комиссий).

- [ ] **Step 1: Создать content-модуль**

```ts
import { SELLER } from '../constants/legal';

export interface LegalSection {
    heading: string;
    paragraphs: string[];
}
export interface LegalDoc {
    title: string;
    intro: string;
    updated: string;
    sections: LegalSection[];
}

export const LEGAL_DOCS: Record<'offer' | 'privacy' | 'refund', LegalDoc> = {
    offer: {
        title: 'Публичная оферта',
        intro: 'Публичная оферта о возмездном оказании услуг сервиса «Лидерра».',
        updated: '2026-06-24',
        sections: [
            { heading: '1. Общие положения', paragraphs: [
                `Настоящий документ является публичной офертой ${SELLER.shortName} (далее — Исполнитель) и адресован любому дееспособному лицу (далее — Заказчик). Акцептом оферты является регистрация в сервисе и/или пополнение баланса.`,
            ] },
            { heading: '2. Предмет', paragraphs: [
                'Исполнитель предоставляет Заказчику доступ к программному сервису «Лидерра» для получения контактов потенциальных клиентов (лидов) по заданным Заказчиком параметрам на условиях предоплаты.',
            ] },
            { heading: '3. Стоимость и оплата', paragraphs: [
                'Услуги оказываются по предоплате путём пополнения внутреннего рублёвого баланса. Стоимость одного лида определяется действующей тарифной ступенью, отображаемой на странице «Цены» и в личном кабинете на момент списания.',
                'Оплата производится банковской картой или через СБП с использованием платёжного сервиса ЮKassa.',
            ] },
            { heading: '4. Порядок оказания услуг', paragraphs: [
                'За каждого переданного Заказчику лида с его баланса списывается стоимость согласно действующему тарифу. Заказчик самостоятельно управляет проектами, лимитами и источниками в личном кабинете.',
            ] },
            { heading: '5. Возврат средств', paragraphs: [
                'Условия возврата определяются Политикой возврата, размещённой по адресу /legal/refund.',
            ] },
            { heading: '6. Персональные данные', paragraphs: [
                'Обработка персональных данных осуществляется согласно Политике конфиденциальности (/legal/privacy) и Федеральному закону № 152-ФЗ.',
            ] },
            { heading: '7. Реквизиты Исполнителя', paragraphs: [
                `${SELLER.legalName}. ИНН ${SELLER.inn}. ОГРНИП ${SELLER.ogrnip}. Адрес: ${SELLER.address}. E-mail: ${SELLER.email}. Телефон: ${SELLER.phone}.`,
            ] },
        ],
    },
    privacy: {
        title: 'Политика конфиденциальности',
        intro: 'Порядок обработки и защиты персональных данных пользователей сервиса «Лидерра» в соответствии с Федеральным законом № 152-ФЗ.',
        updated: '2026-06-24',
        sections: [
            { heading: '1. Оператор', paragraphs: [
                `Оператором персональных данных является ${SELLER.legalName} (ИНН ${SELLER.inn}, ОГРНИП ${SELLER.ogrnip}).`,
            ] },
            { heading: '2. Состав обрабатываемых данных', paragraphs: [
                'Учётные данные Заказчиков (адрес электронной почты, номер телефона) и данные лидов (контакты потенциальных клиентов), обрабатываемые по поручению Заказчика.',
            ] },
            { heading: '3. Цели обработки', paragraphs: [
                'Оказание услуг сервиса, биллинг, техническая поддержка пользователей.',
            ] },
            { heading: '4. Правовые основания', paragraphs: [
                'Согласие субъекта персональных данных, договор-оферта, требования Федерального закона № 152-ФЗ.',
            ] },
            { heading: '5. Хранение и защита', paragraphs: [
                'Персональные данные хранятся на территории Российской Федерации. Применяются разграничение доступа, шифрование, маскирование данных в выгрузках и журналирование действий.',
            ] },
            { heading: '6. Права субъекта', paragraphs: [
                `Субъект персональных данных вправе запросить доступ, уточнение, удаление своих данных и отозвать согласие, направив обращение на ${SELLER.email}.`,
            ] },
            { heading: '7. Контакты', paragraphs: [
                `По вопросам обработки персональных данных: ${SELLER.email}, ${SELLER.phone}.`,
            ] },
        ],
    },
    refund: {
        title: 'Политика возврата',
        intro: 'Условия возврата денежных средств за услуги сервиса «Лидерра».',
        updated: '2026-06-24',
        sections: [
            { heading: '1. Предоплата баланса', paragraphs: [
                'Заказчик пополняет внутренний рублёвый баланс. Неизрасходованный остаток баланса может быть возвращён по заявлению Заказчика на реквизиты, с которых была произведена оплата.',
            ] },
            { heading: '2. Срок возврата', paragraphs: [
                'Возврат неизрасходованного остатка осуществляется в течение 3 (трёх) рабочих дней с момента получения заявления, за вычетом комиссий платёжных систем.',
            ] },
            { heading: '3. Израсходованные средства', paragraphs: [
                'Стоимость уже переданных Заказчику лидов возврату не подлежит, поскольку услуга считается оказанной, за исключением случаев нарушения условий оферты.',
            ] },
            { heading: '4. Порядок обращения', paragraphs: [
                `Заявление о возврате направляется на ${SELLER.email}. Возврат производится тем же способом, которым была произведена оплата.`,
            ] },
        ],
    },
};
```

- [ ] **Step 2: Type-check**

Run: `cd app && npm run type-check`
Expected: без новых ошибок.

- [ ] **Step 3: Commit**

```bash
git add app/resources/js/content/legalDocs.ts
git commit -m "feat(legal): рабочая редакция текстов оферты/политики/возврата"
```

---

## Task 4: LegalDocView — рендер полных текстов

**Files:**

- Modify: `app/resources/js/views/legal/LegalDocView.vue` (полная замена `<script>`+`<template>`)

- [ ] **Step 1: Заменить содержимое view**

```vue
<script setup lang="ts">
/**
 * Правовые документы: оферта, политика конфиденциальности, политика возврата.
 * Контент — из content/legalDocs.ts. Доступны публично (layout 'public').
 * Маршрут /legal/:doc(offer|privacy|refund); иные значения → catch-all 404.
 */
import { computed } from 'vue';
import { useRoute } from 'vue-router';
import { LEGAL_DOCS, type LegalDoc } from '../../content/legalDocs';

const route = useRoute();
const doc = computed<LegalDoc>(() => {
    const key = String(route.params.doc);
    return key === 'privacy' ? LEGAL_DOCS.privacy : key === 'refund' ? LEGAL_DOCS.refund : LEGAL_DOCS.offer;
});
</script>

<template>
    <article class="legal-doc">
        <header class="legal-head">
            <h1 class="text-h4 mb-2">{{ doc.title }}</h1>
            <p class="text-body-1 text-medium-emphasis ma-0">{{ doc.intro }}</p>
            <p class="text-caption text-medium-emphasis">Редакция от {{ doc.updated }}</p>
        </header>
        <section v-for="s in doc.sections" :key="s.heading" class="legal-section">
            <h2 class="text-h6 mb-2">{{ s.heading }}</h2>
            <p v-for="(p, i) in s.paragraphs" :key="i" class="text-body-2 mb-2">{{ p }}</p>
        </section>
    </article>
</template>

<style scoped>
.legal-doc {
    max-width: 760px;
    margin: 0 auto;
    padding: 8px 4px 40px;
}
.legal-head {
    margin-bottom: 24px;
}
.legal-section {
    margin-bottom: 20px;
}
.legal-section h2 {
    font-variation-settings: 'opsz' 18;
}
</style>
```

- [ ] **Step 2: Type-check + lint**

Run: `cd app && npm run type-check && npm run lint:vue`
Expected: без новых ошибок.

- [ ] **Step 3: Commit**

```bash
git add app/resources/js/views/legal/LegalDocView.vue
git commit -m "feat(legal): рендер полных юр-текстов вместо заглушки"
```

---

## Task 5: PublicLayout + AppShell + API + PricingView + роуты

**Files:**

- Create: `app/resources/js/layouts/PublicLayout.vue`
- Create: `app/resources/js/api/public.ts`
- Create: `app/resources/js/views/PricingView.vue`
- Modify: `app/resources/js/components/AppShell.vue`
- Modify: `app/resources/js/router/index.ts`
- Modify: `app/resources/js/layouts/AuthLayout.vue`

- [ ] **Step 1: API-хелпер**

Create `app/resources/js/api/public.ts`:

```ts
export interface PricingTierDto {
    tier_no: number;
    leads_in_tier: number | null;
    price_rub: string;
}

export async function fetchPublicPricing(): Promise<PricingTierDto[]> {
    const res = await fetch('/api/public/pricing', { headers: { Accept: 'application/json' } });
    if (!res.ok) throw new Error(`pricing fetch failed: ${res.status}`);
    const data = (await res.json()) as { tiers: PricingTierDto[] };
    return data.tiers;
}
```

- [ ] **Step 2: PublicLayout**

Create `app/resources/js/layouts/PublicLayout.vue`:

```vue
<script setup lang="ts">
/**
 * Публичный layout (без auth) для юр-страниц и страницы цен.
 * Шапка-бренд + контент (RouterView) + футер с реквизитами/контактами/ссылками.
 * Палитра v8 Forest: page bg warm ivory, бренд teal.
 */
import { RouterLink, RouterView } from 'vue-router';
import { SELLER, LEGAL_LINKS } from '../constants/legal';
</script>

<template>
    <v-app>
        <v-main class="public-main">
            <header class="public-header">
                <RouterLink to="/login" class="public-brand">Лидерра<span class="dot">.</span></RouterLink>
                <nav class="public-nav">
                    <RouterLink v-for="l in LEGAL_LINKS" :key="l.to" :to="l.to">{{ l.label }}</RouterLink>
                </nav>
            </header>
            <div class="public-content">
                <RouterView />
            </div>
            <footer class="public-footer">
                <div class="pf-req">
                    <strong>{{ SELLER.legalName }}</strong><br />
                    ИНН {{ SELLER.inn }} · ОГРНИП {{ SELLER.ogrnip }}<br />
                    {{ SELLER.address }}<br />
                    {{ SELLER.email }} · {{ SELLER.phone }}
                </div>
                <nav class="pf-links">
                    <RouterLink v-for="l in LEGAL_LINKS" :key="l.to" :to="l.to">{{ l.label }}</RouterLink>
                </nav>
            </footer>
        </v-main>
    </v-app>
</template>

<style scoped>
.public-main { background: #f6f3ec; }
.public-header {
    display: flex; align-items: center; justify-content: space-between;
    padding: 20px 32px; border-bottom: 1px solid rgba(1, 32, 25, 0.08); flex-wrap: wrap; gap: 12px;
}
.public-brand { font-weight: 600; font-size: 18px; color: #012019; text-decoration: none; }
.public-brand .dot { color: #0F6E56; }
.public-nav { display: flex; gap: 18px; flex-wrap: wrap; }
.public-nav a { color: #0F6E56; text-decoration: none; font-size: 14px; }
.public-nav a:hover { text-decoration: underline; }
.public-content { padding: 32px; min-height: 50vh; }
.public-footer {
    padding: 28px 32px; border-top: 1px solid rgba(1, 32, 25, 0.08);
    display: flex; justify-content: space-between; gap: 24px; flex-wrap: wrap;
    font-size: 13px; color: #4a5a55;
}
.pf-links { display: flex; flex-direction: column; gap: 6px; }
.pf-links a { color: #0F6E56; text-decoration: none; }
</style>
```

- [ ] **Step 3: PricingView**

Create `app/resources/js/views/PricingView.vue`:

```vue
<script setup lang="ts">
/**
 * Публичная страница цен. Показывает текущую 7-ступенчатую тарифную сетку
 * из /api/public/pricing. Требование ЮKassa: цены публично доступны.
 */
import { onMounted, ref } from 'vue';
import { fetchPublicPricing, type PricingTierDto } from '../api/public';

const tiers = ref<PricingTierDto[]>([]);
const error = ref(false);

function bandLabel(t: PricingTierDto, idx: number): string {
    if (t.leads_in_tier === null) return 'свыше';
    const from = tiers.value.slice(0, idx).reduce((s, x) => s + (x.leads_in_tier ?? 0), 0) + 1;
    const to = from + t.leads_in_tier - 1;
    return `${from}–${to}`;
}

onMounted(async () => {
    try {
        tiers.value = await fetchPublicPricing();
    } catch {
        error.value = true;
    }
});
</script>

<template>
    <section class="pricing">
        <h1 class="text-h4 mb-2">Цены</h1>
        <p class="text-body-1 text-medium-emphasis mb-1">
            Лидерра — сервис получения лидов с оплатой за результат (pay-per-lead).
            Вы пополняете баланс и платите только за переданных лидов.
        </p>
        <p class="text-body-2 text-medium-emphasis mb-6">
            Чем больше лидов за месяц — тем ниже цена за лид. Оплата картой или СБП через ЮKassa.
        </p>

        <v-alert v-if="error" type="warning" variant="tonal" density="compact">
            Не удалось загрузить актуальные тарифы. Обновите страницу.
        </v-alert>

        <v-table v-else class="pricing-table" data-testid="pricing-table">
            <thead>
                <tr>
                    <th>Ступень</th>
                    <th>Лидов в месяц</th>
                    <th class="text-right">Цена за лид</th>
                </tr>
            </thead>
            <tbody>
                <tr v-for="(t, i) in tiers" :key="t.tier_no">
                    <td>{{ t.tier_no }}</td>
                    <td>{{ bandLabel(t, i) }}</td>
                    <td class="text-right pricing-num">{{ t.price_rub }} ₽</td>
                </tr>
            </tbody>
        </v-table>

        <p class="text-caption text-medium-emphasis mt-4">
            Цены указаны в рублях за одного лида. Тарифные ступени применяются накопительно в течение месяца.
        </p>
    </section>
</template>

<style scoped>
.pricing { max-width: 760px; margin: 0 auto; }
.pricing-num { font-family: 'JetBrains Mono', ui-monospace, monospace; font-variant-numeric: tabular-nums; }
.pricing-table :deep(th) { font-weight: 600; }
</style>
```

- [ ] **Step 4: AppShell — ветка public**

В `app/resources/js/components/AppShell.vue`:

- добавить импорт: `import PublicLayout from '../layouts/PublicLayout.vue';`
- в `<template>` добавить ветку перед `<AppLayout v-else />`:

```vue
    <PublicLayout v-else-if="layoutName === 'public'" />
```

- обновить doc-комментарий: `meta.layout = 'public' → PublicLayout (юр-страницы и цены, без auth)`.

- [ ] **Step 5: Роутер — legal+refund, layout public, /pricing**

В `app/resources/js/router/index.ts`:

- legal-роут заменить на:

```ts
    {
        path: '/legal/:doc(offer|privacy|refund)',
        name: 'legal',
        component: () => import('../views/legal/LegalDocView.vue'),
        meta: { layout: 'public', title: 'Правовые документы' },
    },
```

- добавить новый роут (например, после legal):

```ts
    {
        path: '/pricing',
        name: 'pricing',
        component: () => import('../views/PricingView.vue'),
        meta: { layout: 'public', title: 'Цены' },
    },
```

- [ ] **Step 6: AuthLayout — ссылки в футер логина**

В `app/resources/js/layouts/AuthLayout.vue`, в блок `.bp-foot` после ссылки «Политика» добавить:

```vue
                            <RouterLink to="/legal/refund">Возврат</RouterLink>
                            <RouterLink to="/pricing">Цены</RouterLink>
```

- [ ] **Step 7: Type-check + lint + build**

Run: `cd app && npm run type-check && npm run lint:vue && npm run build`
Expected: 0 ошибок, build OK.

- [ ] **Step 8: Commit**

```bash
git add app/resources/js/layouts/PublicLayout.vue app/resources/js/api/public.ts app/resources/js/views/PricingView.vue app/resources/js/components/AppShell.vue app/resources/js/router/index.ts app/resources/js/layouts/AuthLayout.vue
git commit -m "feat(public): публичный layout, страница цен и публичные юр-ссылки"
```

---

## Task 6: Живая визуальная проверка (Playwright, локалка)

> vitest в проекте сломан — фронт проверяем живым браузером (см. память). Запустить dev-сервер локально, пройти по публичным страницам без логина.

**Files:** нет (проверка).

- [ ] **Step 1: Поднять локалку**

Run: `cd app && php artisan serve` (+ `npm run dev` при необходимости). Открыть `http://127.0.0.1:8000`.

- [ ] **Step 2: Пройти публичные страницы БЕЗ логина**

Через Playwright MCP проверить (ожидаемый результат рядом):

- `/pricing` — таблица `data-testid="pricing-table"`, 7 строк, цены 70…40 ₽, ступень 7 = «свыше». Футер с реквизитами ИП (ИНН 245210851872, ОГРНИП 326246800098042, info@liderra.ru).
- `/legal/offer` — заголовок «Публичная оферта», 7 разделов, реквизиты в §7.
- `/legal/privacy` — «Политика конфиденциальности», 7 разделов.
- `/legal/refund` — «Политика возврата», срок «3 (трёх) рабочих дней».
- `/login` — в футере бренд-панели ссылки «Оферта», «Политика», «Возврат», «Цены» — все ведут на нужные страницы (не 404).

- [ ] **Step 3: Зафиксировать результат**

Краткий отчёт: что открылось/что нет, скриншоты `/pricing` и `/legal/offer`. Если что-то не так — чинить до зелёного.

---

## Task 7: Финальная сверка + хэндофф владельцу

**Files:** нет (отчёт).

- [ ] **Step 1: Сводка для владельца**

Подготовить: что сделано, ссылки на страницы, что осталось на нём — (а) **вычитать юр-тексты** перед прод (в `content/legalDocs.ts`), (б) дать GO на деплой. Деплой на прод — отдельным шагом по `deploy/README.md` (фронт-build + overlay), НЕ в этом плане.

- [ ] **Step 2: НЕ деплоить без GO.** Прод не трогаем. Коммиты — в gitea по escape владельца.

---

## Self-Review

**Spec coverage** (требования ЮKassa из DRAFT §4):

- Оферта → Task 3+4 ✓
- Политика конфиденциальности → Task 3+4 ✓
- Политика возврата → Task 3+4 ✓
- Реквизиты продавца (ИНН/ОГРНИП/наименование/адрес) → Task 2 + футер Task 5 ✓
- Контакты (телефон/почтовый адрес/e-mail) → Task 2 + футер ✓
- Цены публично → Task 1 (эндпоинт) + Task 5 (PricingView) ✓
- Описание услуг → PricingView intro + оферта §2 ✓
- Все ссылки рабочие (не 404) → Task 5 (роуты) + Task 6 (проверка) ✓
- Оплата без редиректа → уже соблюдено (вне scope) ✓

**Placeholder scan:** тексты юр-документов — рабочая редакция (не «TODO»); помечены на вычитку владельца (это его явное решение, не плейсхолдер кода).

**Type consistency:** `PricingTierDto` (tier_no/leads_in_tier/price_rub) одинаков в `api/public.ts` и `PricingView.vue`; `LegalDoc`/`LegalSection` — в `content/legalDocs.ts` и `LegalDocView.vue`; `SELLER`/`LEGAL_LINKS` — в `constants/legal.ts`, потребители Task 3/5. price_rub — строка (`number_format`) и на бэке, и на фронте.

---

## История

| Дата | Кто | Что |
|---|---|---|
| 2026-06-24 | Claude | План создан. Реализация фич под модерацию ЮKassa: публичные юр-страницы + реквизиты + страница цен. Прод НЕ трогается; юр-тексты — на вычитку владельцу. |
