«Симуляція вступу»
Метадані
Section titled “Метадані”| Поле | Значення |
|---|---|
| Статус | 🟢 prod — лендинг, конструктор і paywall змержені в main і віддаються живим сайтом. ⚠️ Файл-специфікація docs/domains/simulation/overview.md (2026-06-18) ще каже «коду фічі ще немає» — застаріло: фічу зібрано пізніше (PR #434 → аналітика #436 → drag-reorder #441). |
| Поверхні | web (публічний лендинг /simulation + кабінетний конструктор /profile/simulation) + безкоштовний тізер на сторінці оффера (ApplicantsCard → вкладки «Симуляція»). Бот має власний аналог («Куди я вступлю?») — це інша реалізація. |
| Доступність | будувати/зберігати вступліст і рахувати КБ — безкоштовно (потрібен акаунт); присуд (де проходиш · статус · запас · «згорілі» пріоритети) — premium one-off (разова покупка simulation, без підписки, доступ до кінця сезону SIMULATION_SEASON_END). Прайс — серверний (≈ 399 ₴, див. Дані та API). |
| Власник | фронт — Шура; алгоритм/КБ — Льоша (за docs/domains/simulation/overview.md) |
| Останнє підтвердження | 2026-06-27 · curl (live: /uk/simulation 200, /uk/simulation/success 200, /profile/simulation → 307 /uk/auth/login) + код:main (frontend 982662a6, api-v2 6a1d530) |
Призначення
Section titled “Призначення”«Симуляція вступу» відповідає на головне тривожне питання абітурієнта-2026: «Чи я проходжу на бюджет?» Замість збирати вступну стратегію вручну (Excel із торішніми балами, зошит, десяток вкладок ЄДЕБО, скрін у телефоні, поради в чатах) — жоден із цих способів не каже, чи ти реально вступаєш — абітурієнт вводить бали НМТ і свій вступліст (список КП — зв’язок ЗВО + спеціальність + фінансування), а інструмент проганяє його через правила розподілу й показує по кожній заяві, куди він проходить і з яким запасом балів.
Це продуктизована, монетизована вершина воронки шансів: калькулятор конкурсного бала → «Мій бал» → wizard «Куди я вступлю?» → симуляція пріоритетного списку.
Цінність для абітурієнта (free vs paid)
Section titled “Цінність для абітурієнта (free vs paid)”Із лендингового блоку «Одна оплата — без підписки» (PricingSection):
| Безкоштовно (0 ₴, «назавжди») | Повний доступ (разова оплата) |
|---|---|
| пошук конкурсних пропозицій | показуємо, де ти проходиш на бюджет |
| розрахунок конкурсного бала (КБ) | статус по кожній заяві: проходиш / на межі / ні |
| збереження вступліста | запас балів від прохідного по кожній заяві |
| прохідні бали минулих років | які пріоритети «згорають» і чому |
| базові тести НМТ | перевірка списку (ліміти й мінімальний бал) |
| безстроковий доступ до результату на сезон |
Коротко: зібрати й зберегти вступліст і порахувати КБ — безкоштовно; сам присуд (де проходиш, статус, запас, які пріоритети згорають) за офіційним алгоритмом розподілу — платний анлок. Безкоштовна CTA веде в наявний застосунок (/offers/result), платна — у checkout.
⚠️ Що працює зараз (Стан 1) vs обіцяне (Стан 2)
Section titled “⚠️ Що працює зараз (Стан 1) vs обіцяне (Стан 2)”Команда розвела дві механіки (docs/domains/simulation/overview.md):
- Стан 1 — конструктор + статус (зараз у проді): додавання КП, намір бюджет/контракт, шанс (КБ vs торішній прохідний-2025) і запас, drag-пріоритети. Алгоритм широкого конкурсу не потрібен.
- Стан 2 — симуляція широкого конкурсу (відкладено, після старту подачі 19.07, на живих даних кампанії-2026): поточний прохідний-2026 (денний бекенд-прогон) + вердикт (deferred-acceptance, суперобсяг, субконкурси).
Наслідок: кнопка «Перерахувати» в проді запускає клієнтський placeholder-движок вердикту (utils/simulate.ts), а не справжній алгоритм розподілу. Реальний прохідний-2026 і вердикт — це Стан 2, ще не побудований. Шанс/запас на даних 2025 — справжні.
User flow
Section titled “User flow”Точки входу
Section titled “Точки входу”Публічний лендинг: https://abitly.org/uk/simulation (локаль-префікс завжди є; сторінка індексується). Конструктор: https://abitly.org/profile/simulation (кабінет, noindex, гейт за логіном і entitlement). Сторінка-повернення: /uk/simulation/success.
Конструктор (/profile/simulation)
Section titled “Конструктор (/profile/simulation)”Гейт: paywall за SIMULATION_PAYWALL_ENABLED. Залогінений користувач без entitlement simulation бачить недимісабельну SimulationPaywallModal («Купити» → checkout, «Дізнатись більше» → лендинг) замість конструктора; з entitlement — рендериться SimulationDashboard.
- Вхід у конструктор —
SimulationDashboard(season-banner Стану-1,ScoresCard, список, завжди змонтованаAddKpModal); на устаканенні списку — подіяsimulation_constructor_viewed. - Бали НМТ —
ScoresCard: обов’язкові (укр/мат/історія) + 4-й предмет + бонус-конкурс; валідація 100–200; «Зберегти» персистить grades на бекенд →simulation_scores_saved. - Порожній стан —
SimulationEmpty(CTA → відкрити «Додати КП»;simulation_add_kp_openedsource=empty_state). - Додати КП — модалка (на мобілці bottom-sheet): вкладки «Пошук усіх КП» (typeahead + фасет-чіпи регіон/ЗВО/спеціальність/форма) і «Мої вступлісти»; мультивибір (≤30 на сторінку), намір авто-проставляється (контракт-only → контракт; інакше бюджет, поки є бюджетні слоти). Apply шле один
applyChanges→simulation_kp_added/simulation_kp_removedsource=modal. - Перегляд списку — режими Таблиця / Картки / Редагування; кожен рядок несе тріаду Намір · Шанс · Запас; тулбар показує
X/10загалом іY/5на бюджет. - Редагування пріоритетів і наміру — drag/стрілки (debounce 400 мс →
reorder, подіяsimulation_priorities_reordered), сегмент Бюджет/Контракт (updateIntent; при повному бюджеті →SwapIntentModal→swapIntent), дублювання на протилежний намір, видалення; автозбереження. - Запуск симуляції —
SimulateBand«Перерахувати» (активна за ≥1 заяви та ≥1 балів); клієнтський прогін → вердикт на рядок; зміна списку після прогону →isStaleховає колонку вердикту. - Результат —
FeedbackCard(«що варто запам’ятати») + чіп вердикту на картці/рядку.
Тріада (навмисно різнокольорова): Намір — куди хочеш (бюджет→контракт / тільки контракт); Шанс — де реально опинишся за балом (на бюджет / на контракт / не проходиш); Запас — розрив КБ до прохідного рівня наміру. Ліміти (серверні): ≤10 заяв, ≤5 на бюджет.
Покупка та анлок (sequence)
Section titled “Покупка та анлок (sequence)”Checkout переюзає Monobank-флоу. Специфіка симуляції — анлок видає feature-entitlement (не файл) і сторінка-success полить статус:
sequenceDiagram
autonumber
participant U as Абітурієнт
participant W as frontend (web)
participant A as api-v2 (premium)
participant M as Monobank
participant DB as Postgres
U->>W: Купити (лендинг / paywall-модалка)
W->>A: GET /premium/simulation/product
A-->>W: { productId, priceUah }
W->>A: POST /premium/create-payment {productId, productType:simulation}
A->>DB: premium_orders ← pending (amount = серверний прайс)
A->>M: create invoice (sum, redirectUrl=/simulation/success?orderId)
A-->>W: { orderId, paymentUrl }
W-->>U: window.location → Monobank (pay.mbnk.biz)
U->>M: оплата
M->>A: webhook /premium/webhook/monobank (x-sign)
A->>DB: premium_orders → success · grant entitlement (user_feature_overrides simulation=true)
A->>U: лист «Симуляцію розблоковано»
U->>W: повернення на /simulation/success?orderId
W->>A: GET /premium/order-status (poll кожні 2с)
A-->>W: success → інвалідація entitlements
W-->>U: redirect на /profile/simulation (конструктор розблоковано)
Воронки та аналітика (що зараз реалізовано)
Section titled “Воронки та аналітика (що зараз реалізовано)”Pipeline: track() → window.dataLayer → GTM GTM-W64R6XXK → GA4 G-XKDRSM3T0Z (dataLayer фаєриться лише на хості abitly.org/www.abitly.org — QA через GA4 DebugView на проді). Деталі та план — docs/domains/simulation/attribution.md у репо фронтенду.
Meta Pixel (для платної реклами): на success-сторінці, всередині isPaid-guard’а, окрім simulation_purchase тепер фаєриться і fbq('track','Purchase', {value, currency}, {eventID: orderId}) у піксель 4273494836207329 (FE PR #497/#499, prod) — щоб кампанії оптимізувались на реальну покупку, а не на проксі. Контекст — Платна реклама Meta.
Точки входу у воронку
Section titled “Точки входу у воронку”| Поверхня | Що це | Подія / source |
|---|---|---|
| Тізер на сторінці оффера — вкладка «Симуляція» | SimulationApplicantsTable (симульований ранжований список заявок цього оффера на даних 2025); анонімам розмито під AuthPopupOverlay → реєстрація | click_on_admission_simulation {offer_id, university_id} |
| Тізер — вкладка «Симуляція + сортування» | той самий віджет, інший режим | click_on_admission_simulation_sorting |
Лендинг /simulation (публічний, індексований) | головна UTM-ціль зовнішніх кампаній | simulation_landing_view {is_authenticated} |
| Лендинг — CTA «Спробувати» (hero / «як це працює») | у безкоштовний конструктор (не checkout) | simulation_try_clicked {placement} |
| Лендинг — кнопки «Купити» (pricing / final CTA) | у Monobank-checkout | simulation_begin_checkout source=simulation_landing_pricing | simulation_landing_cta |
| Paywall-модалка в кабінеті «Купити» | головний in-product вхід у платну воронку | simulation_begin_checkout source=paywall_modal |
| Навігація кабінету (sidebar + header) | лінк на /profile/simulation | лише навігація, без події |
| Сторінка-success | підтвердження покупки | simulation_purchase {transaction_id} |
Шлях конверсії
Section titled “Шлях конверсії”flowchart LR
subgraph FREE[Безкоштовний верх воронки]
O[Сторінка оффера → тізер «Симуляція»] -->|анонім| R[AuthPopupOverlay → реєстрація]
end
subgraph PAID[Платний верх воронки]
L[Лендинг /simulation] -->|Спробувати| C0[Конструктор free]
L -->|Купити| CO
end
R --> C0
CO[Monobank checkout] --> S[success-сторінка: poll статусу]
S --> E[entitlement simulation]
E --> C[Конструктор розблоковано]
C --> F[11-подійна продуктова воронка]
Реалізовані події (16 типізованих)
Section titled “Реалізовані події (16 типізованих)”Paywall / ecommerce (3):
| Подія | Коли | Примітка |
|---|---|---|
simulation_begin_checkout | onMutate create-payment | несе source, value, items |
simulation_payment_init_failed | onError create-payment | несе source + product_id |
simulation_purchase | success-page, isPaid | transaction_id=orderId; без source (редірект Monobank стирає стан FE) |
Продуктова воронка + лендинг (11, PR #436): simulation_constructor_viewed, add_kp_opened, add_kp_searched (PII-safe: довжина/кількість, не текст), kp_added (core), kp_removed, view_mode_changed, intent_changed (core), priorities_reordered (core), scores_saved (core), landing_view, try_clicked. Чокпоінти (single-source): useAddKp.flush, useEditReorder.commitOrder, useScoresForm, useSimulationPurchase.
Тізер оффера (2): click_on_admission_simulation, click_on_admission_simulation_sorting.
PII-безпека: лише ID (offer_id), ніколи назв ЗВО/спеціальностей; пошук шле has_query/query_length/results_count, не сирий текст; scores_saved — лише булеві прапори, не бали.
Стан GTM/GA4 та прогалини
Section titled “Стан GTM/GA4 та прогалини”- Зроблено (інструментарієм):
gtm:sync --applyзастейджив усі події у GTM (workspace 86);ga4:dimensions:syncзареєстрував параметри як event-scoped custom dimensions (⚠️ property на 49/50 — близько ліміту; dimensions не ретроактивні). - Не зроблено (людські кроки): ручний PUBLISH GTM-workspace; ремап
simulation_purchase→ подіяpurchase+ Ecommerce (інакше доходу не видно в GA4 Monetization); pre-launch «List unwanted referrals» =pay.mbnk.biz(нерероактивно — інакше повернення з Monobank псує атрибуцію сесій покупки); UTM-конвенція для маркетингу; smoke на/offers/result+ DebugView. - Свідома прогалина: покупку-по-поверхні не несемо через редірект — її читають когортно в GA4 через лійку
begin_checkout(source) → purchase.fbclidневидимий для GA4 → лінки Meta/TikTok мусять мати UTM.
Дані та API
Section titled “Дані та API”| Джерело | Ендпоінт / entity | Призначення |
|---|---|---|
| api-v2 | GET /profile/simulation · POST /…/applications · PATCH/DELETE /…/applications/:id · POST /…/applications/swap-intent · PUT /…/order · POST /…/hydrate · GET /…/offers · GET /…/search-filters | CRUD конструктора (SimulationController). ⚠️ гейт лише JwtAuthGuard — серверного entitlement-guard на цих ендпоінтах немає |
| api-v2 | GET /premium/simulation/product (public, Redis-cached) · POST /premium/create-payment · GET /premium/order-status · POST /premium/webhook/monobank | прайс + checkout + статус + вебхук (спільний premium-модуль) |
| api-v2 | GET /subscriptions/features | читання entitlement simulation (FE entitlementsApi.getMine → hasFeature) |
| Postgres | simulation_application | дані конструктора: userId + offerId + intent + priority; UNIQUE(userId, offerId, intent); ліміти ≤10 / ≤5 |
| Postgres | premium_orders | покупка (product_type='simulation', amount у копійках, status) — деталі |
| Postgres | user_feature_overrides | де живе entitlement: feature_key='simulation', value='true', expires_at=кінець сезону, reason='premium_order:<id>' |
Entitlement. Вебхук Monobank на success + productType=simulation → grantSimulationEntitlement (ідемпотентний upsert у user_feature_overrides). Читання — FeatureResolverService (override → план активної підписки → дефолт free-плану). Гейт paywall — лише фронтовий (гейт сторінки + paywall-модалка) + сам факт оплаченого ордера; серверного feature-guard на CRUD немає.
Прайс — серверний, FE суми не шле. src/api/premium/simulation-product.config.ts: priceCoins = SIMULATION_PRICE_KOPEKS (копійки; дефолт-плейсхолдер 1000 = 10 ₴, у проді перевизначено через SSM ≈ 39900 = 399 ₴), expiresAt = SIMULATION_SEASON_END. Назви env — в environments. Читання прайсу Redis-кешоване (12 год) → зміна прайсу потребує скиду кешу. FE відображає priceUah; копія «399 ₴» — лише fallback до завантаження.
Зв’язки з іншими фічами
Section titled “Зв’язки з іншими фічами”- «Мої пріоритети» — безкоштовний док-компаньйон того ж списку (
simulation_application): збирай/сортуй/дивись статус без пейволу на всіх публічних сторінках, апсел-CTA веде в цей платний вердикт. Верх воронки. - Каталог спеціальностей / офферів — на сторінці оффера живе безкоштовний тізер (
ApplicantsCard→ вкладки «Симуляція / Симуляція + сортування»), верх воронки → реєстрація. - Checkout Monobank — спільний
premium-модуль і таблицяpremium_orders; цей же шлях оплати. - Реферальна програма — 🚧 промокод друга на buy-поверхнях симуляції; 100 грн рефереру за покупку.
- Тарифи Free/Pro/Max + tokens — entitlement-механіка (
user_feature_overrides/subscriptions/features); симуляція — разовий продукт, не підписка. - Калькулятор конкурсного бала, «Мій бал», wizard «Куди я вступлю?» — попередні щаблі воронки шансів; КБ-логіка спільна.
- Збережене / Вступлісти — вкладка «Мої вступлісти» в модалці додавання КП.
- Особистий кабінет · Автентифікація — конструктор живе в кабінеті за логіном.
| Репо | Шлях | Примітка |
|---|---|---|
abitly-frontend-v2 | src/app/[locale]/(main)/simulation/ · (profile)/profile/simulation/ | лендинг + success + конструктор (роути) |
abitly-frontend-v2 | src/components/pages/simulation/** | лендинг, success, спільний buy-флоу (useSimulationPurchase) |
abitly-frontend-v2 | src/components/pages/profile/pages/Simulation/** | SimulationDashboard + Scores/AddKp/Table/Cards/Edit/Bands + хуки |
abitly-frontend-v2 | src/components/pages/offers/pages/search/OfferPage/components/SimulationApplicantsTable/** | безкоштовний тізер на сторінці оффера |
abitly-frontend-v2 | src/api/simulation/** · src/lib/analytics/events/simulation.ts | API-клієнти + типізовані події |
abitly-frontend-v2 | docs/domains/simulation/** | першоджерело дизайну (overview / mechanics / attribution / components) — детальніше за цю картку |
abitly-api-v2 | src/api/simulation/** · src/api/premium/** (+ simulation-product.config.ts) | CRUD + checkout + вебхук + прайс/сезон |
abitly-api-v2 | src/database/entities/{simulationApplication,premiumOrder,userFeatureOverride}.ts | entities + міграції |
Обмеження та блокери
Section titled “Обмеження та блокери”- Серверний paywall відсутній на CRUD.
SimulationControllerгейтить лишеJwtAuthGuard; entitlementsimulationперевіряється тільки на фронті + наявністю оплаченого ордера. Будь-який залогінений може дернути CRUD-ендпоінти симуляції напряму.TODO:додати feature-guard на бекенді. - Вердикт у проді — клієнтський placeholder (
utils/simulate.ts), не справжній алгоритм розподілу. Реальний широкий конкурс + прохідний-2026 (Стан 2) — після 19.07, ще не побудовано. - Атрибуція покупки не запущена до кінця: GTM-workspace застейджено, але не опубліковано;
purchase→Monetization-ремап і Monobank-referral-exclusion (pay.mbnk.biz, нерероактивно) — невиконані людські кроки. До цього дохід не видно в GA4 і рання атрибуція псується. - Прайс кешований у Redis 12 год — зміна
SIMULATION_PRICE_KOPEKSбез скиду ключа*getSimulationProduct*дає розбіжність відображення/списання до 12 год. order_product_typeenum-drift /buyer_emailvsemailу міграціїpremium_orders— позначено агентом для звірки.TODO:верифікувати.reversed(повернення) не відкликає entitlement — доступ гасне лише поexpires_at(кінець сезону).TODO:уточнити політику рефандів.
Історія змін
Section titled “Історія змін”| Дата | Подія | Джерело |
|---|---|---|
| 2026-06-18 | дизайн-специфікація Стану 1/2 (тріада, ліміти, mechanics) | docs/domains/simulation/overview.md |
| 2026-06-25 | дизайн атрибуції/UTM | docs/domains/simulation/attribution.md |
| 2026-06-26 | +11 типізованих продуктових подій | PR #436 (frontend) |
| 2026-06-27 | drag-reorder пріоритетів у таблиці | PR #441 (982662a6) |
| 2026-06-27 | single-origin фікс email/redirect URL у premium | PR #526 (api-v2 6a1d530) |
| 2026-06-27 | створено картку 🟢; підтверджено live (curl) + код:main | ця картка |