RunAutopodborSearchJob по завершении (done/empty) шлёт AutopodborReadyMail на contact_email тенанта
(с числом найденных + ссылкой на портал). Письмо не роняет подбор при недоступной почте (try/catch).
Клиент ставит задачу, работает дальше, получает «готово» письмом.
Тесты: SearchJob 4/4 (вкл. Mail::assertSent); Автоподбор unit+feature 183/183; Pint чисто.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Шаг 2 «Конкурентного поля»: один номер встречается в нескольких местах —
код сайта плюс карточки 2ГИС/Яндекс с разными адресами. Раньше хранилось
одно provenance_url/label — список терялся. Теперь сквозной провод
движок→контракт→джоб→БД→API; фронт уже умел показывать кликабельным
списком с подтверждениями.
- autopodbor_sources +3 колонки where_found/office/confirmations
миграция 2026_06_30_120000, идемпотентная, RLS-review APPROVE 7/7
- canon-sync schema.sql v8.59 плюс CHANGELOG, вкл. catch-up phone_type/box 29.06
- тесты бэкенда автоподбора 122/122
НЕ на проде, воркстри avtopodbor.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Фича «Конкурентное поле» на dev до уровня прототипа 2026-06-29-konkurentnoe-pole-proto.html.
Данные: box (proposal|field) на competitors+sources; phone_type city/mobile/tollfree рядом
с phone_kind (вариант C). 3 миграции, дефолты тарифов 300/50.
API (AutopodborController): GET /field (+счётчики), GET /proposals, PATCH/DELETE competitors
и sources с гвардами активного проекта, переключение box, POST /competitors/manual (+directory_urls),
competitor(id) обогащён box+project-статусом; projectStatus отдаёт limit/delivered/days/regions.
Смена источника проекта = PATCH /api/projects/{id} (реальный гвард слепка §14.10).
Фронт: FieldWorkspaceScreen/FieldCompetitorScreen/FieldProposalsScreen/FieldManualCompetitorScreen
+ field-shared.css (Forest) + AutopodborServicesPanel в Биллинге. Дословно по прототипу: подзаголовки,
баннер предложений, баннер правил времени 18:00 МСК, Справочник 2ГИС·Яндекс, статус проекта
5/день·заявки, окна сбора с ценами 300/50 + «что известно», полные формы. Пункт меню «Конкурентное поле».
Тесты: backend автоподбор 80/80, фронт автоподбор 49/49. Движок шага 2 = заглушка FakeCompetitorAgent.
OmegaDemoFieldSeeder — только для визуальной проверки (НЕ на прод).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- AutopodborChargeService::chargeForRun — DB::transaction + lockForUpdate
на AutopodborRun (guard идемпотентности по balance_transaction_id) и Tenant;
bcmath (bcsub/bccomp/bcmul), никаких float; throw InsufficientBalanceException
до любых изменений баланса при нехватке средств.
- Миграция 2026_06_28_110100: расширяет CHECK constraint
balance_transactions_type_check — добавляет 'autopodbor_charge'.
- Тест: 2 money-инварианта (идемпотентность + noop при нехватке).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>