diff --git a/students/Kulikovskaya_Alina/lab-01/analysis.md b/students/Kulikovskaya_Alina/lab-01/analysis.md new file mode 100644 index 00000000..c7ce1d55 --- /dev/null +++ b/students/Kulikovskaya_Alina/lab-01/analysis.md @@ -0,0 +1,133 @@ +# Анализ транзакционных границ и ответственности + +## 1. Границы транзакции + +### Где начинается транзакция? +**Точка входа:** Пользователь нажимает кнопку "Забронировать с оплатой" + +### Где заканчивается транзакция? +**Успешный сценарий:** После отправки email-подтверждения и записи события в лог + +**Граница транзакции охватывает:** +1. Блокировка слота в Schedule Service +2. Создание записи бронирования в БД +3. Обработка платежа +4. Подтверждение бронирования +5. Отправка уведомления (асинхронно, вне транзакции) + + +## 2. Таблица операций + +| Операция | Тип | Откат при ошибке | Retry-стратегия | Идемпотентность | +|----------|-----|------------------|-----------------|-----------------| +| **Блокировка слота** | Синхронная | Да (release lock) | Нет (TTL 10 минут) | Да (idempotency_key = booking_id) | +| **Создание бронирования в БД** | Синхронная | Да (DELETE) | Нет (PK constraint) | Да (проверка уникальности user+slot+date) | +| **Вызов Payment Gateway** | Синхронная | Нет (async проверка) | 3 попытки с exponential backoff (1s, 2s, 4s) | Да (payment_intent_id) | +| **Обновление статуса бронирования** | Синхронная | Да (ROLLBACK) | Нет | Да | +| **Отправка email** | **Асинхронная** | Нет (best-effort) | 5 попыток с exp. backoff | Да (deduplication по booking_id) | +| **Освобождение слота при отмене** | Синхронная | Нет | Нет | Да | + + +## 3. Обоснование выбора синхронных/асинхронных операций + +### Почему блокировка слота — синхронная? +- **Критичность:** Двойное бронирование недопустимо +- **Консистентность:** Необходима immediate consistency +- **Откат:** При ошибке платежа слот должен освободиться + +### Почему отправка email — асинхронная? +- **Некритичность:** Пользователь уже видит страницу успеха +- **Надёжность:** Email может быть недоступен временно +- **Производительность:** Не блокировать ответ пользователю +- **Eventual consistency:** Email дойдёт через несколько секунд/минут + +### Почему платёж — синхронный с fallback? +- **UX:** Пользователь ждёт immediate feedback +- **Сложность:** Требуется подтверждение перед финализацией бронирования +- **Fallback:** При таймауте → статус PENDING_PAYMENT + async проверка + + +## 4. Обработка исключительных ситуаций + +### 4.1 Race Condition (двойное бронирование) + +**Условие:** Два пользователя одновременно бронируют последний слот + +**Обнаружение:** +- Schedule Service возвращает ошибку "Slot already locked" +- Или БД возвращает constraint violation при INSERT + +**Реакция:** +1. Транзакция второго пользователя откатывается +2. Система предлагает альтернативные слоты +3. Логируется попытка конфликтного доступа + +**Компенсация:** +- Не требуется (атомарная проверка в Schedule Service) +- Слот остаётся заблокированным за первым пользователем + +**Уведомление:** +- "Извините, этот слот только что заняли. Выберите другое время: 19:00-20:00 или 20:00-21:00" + + +### 4.2 Таймаут Payment Gateway + +**Условие:** Платёжный шлюз не отвечает > 30 секунд + +**Обнаружение:** +- HTTP-клиент выбрасывает TimeoutException +- Circuit Breaker открывается после 3 таймаутов + +**Реакция:** +1. НЕ откатывать бронирование сразу (возможно, платёж прошёл) +2. Перевести статус в "PAYMENT_PENDING" +3. Запустить фоновую задачу проверки статуса (через 1, 5, 15 минут) +4. Блокировка слота продлевается до 30 минут + +**Компенсация (если платёж не прошёл):** +- Через 15 минут: статус → CANCELLED +- Освободить слот в Schedule Service +- Уведомить пользователя об отмене + +**Уведомление:** +- Немедленно: "Проверяем статус платежа... Не закрывайте страницу" +- Через 15 мин (если failed): "Бронирование отменено. Попробуйте снова." + + +### 4.3 Недостаточно средств + +**Условие:** Payment Gateway возвращает "Insufficient funds" + +**Обнаружение:** +- HTTP 200 с body: `{status: "failed", reason: "insufficient_funds"}` + +**Реакция:** +1. Сохранить бронирование со статусом "PAYMENT_FAILED" +2. Предложить варианты: другая карта / оплата на месте / отмена +3. Установить таймер на 10 минут для автоматической отмены + +**Компенсация:** +- Если пользователь не выбирает действие → автоматическая отмена +- Освобождение слота + +**Уведомление:** +- "Недостаточно средств на карте. Попробуйте другую карту или оплатите на ресепшене." + + +### 4.4 Нарушение бизнес-правила (минимальное время) + +**Условие:** Попытка бронирования < 30 минут до начала слота + +**Обнаружение:** +- Валидация на уровне Application Layer: `if (slot_start - now) < 30min` + +**Реакция:** +1. Отклонить запрос до создания бронирования +2. Предложить позвонить администратору + +**Компенсация:** +- Не требуется (бронирование не создавалось) + +**Уведомление:** +- "Слишком поздно для online-бронирования. Звоните: +375 (17) 123-45-67" + diff --git a/students/Kulikovskaya_Alina/lab-01/diagrams/sequence-error-payment.png b/students/Kulikovskaya_Alina/lab-01/diagrams/sequence-error-payment.png new file mode 100644 index 00000000..86a9a7e5 Binary files /dev/null and b/students/Kulikovskaya_Alina/lab-01/diagrams/sequence-error-payment.png differ diff --git a/students/Kulikovskaya_Alina/lab-01/diagrams/sequence-error-payment.puml b/students/Kulikovskaya_Alina/lab-01/diagrams/sequence-error-payment.puml new file mode 100644 index 00000000..8ad66549 --- /dev/null +++ b/students/Kulikovskaya_Alina/lab-01/diagrams/sequence-error-payment.puml @@ -0,0 +1,86 @@ +@startuml +!theme plain +skinparam sequenceMessageAlign center + +title Бронирование - Ошибка: Таймаут платёжного шлюза + +actor "Пользователь" as User +participant "Web UI" as UI +participant "Booking API" as API +database "PostgreSQL" as DB +participant "Payment Gateway" as Payment +participant "Scheduler\n(Cron)" as Cron +participant "Notification Service" as Notify + +User -> UI: Выбрать слот, нажать "Забронировать" +activate UI +UI -> API: POST /api/bookings +activate API +API -> DB: INSERT status: PENDING_PAYMENT +activate DB +DB --> API: booking_id: BK-2025-00143 +deactivate DB +API --> UI: 201 + payment_url +deactivate API +UI --> User: Страница оплаты +deactivate UI + +User -> UI: Подтвердить оплату +activate UI +UI -> Payment: Charge booking BK-2025-00143 +activate Payment + +note right of Payment + Платёжный шлюз не отвечает + > 30 секунд +end note + +Payment --> UI: **TimeoutException** +deactivate Payment + +UI -> API: POST /bookings/BK-2025-00143/timeout +activate API + +API -> DB: UPDATE status='PAYMENT_PENDING' +activate DB +DB --> API: OK +deactivate DB + +API -> Cron: Запланировать проверку\nчерез 1, 5, 15 минут +activate Cron +Cron --> API: Scheduled +deactivate Cron + +API --> UI: 202 Accepted\n"Проверяем статус платежа..." +deactivate API + +UI --> User: "Обработка платежа...\nНе закрывайте страницу" +deactivate UI + +Cron -> API: GET /internal/bookings/BK-2025-00143/check-payment +activate API +API -> Payment: Query payment status +activate Payment +Payment --> API: Status: UNKNOWN/FAILED +deactivate Payment + +API -> DB: UPDATE status='CANCELLED' +activate DB +DB --> API: OK +deactivate DB + +API -> DB: UPDATE slot SET status='AVAILABLE' +activate DB +DB --> API: OK +deactivate DB + +API -> Notify: Отправить уведомление об отмене +activate Notify +Notify --> API: OK +deactivate Notify + +deactivate API + +Notify -> User: Email: "Бронирование отменено\nПлатёж не прошёл" + +@enduml diff --git a/students/Kulikovskaya_Alina/lab-01/diagrams/sequence-happy.png b/students/Kulikovskaya_Alina/lab-01/diagrams/sequence-happy.png new file mode 100644 index 00000000..3ac432d0 Binary files /dev/null and b/students/Kulikovskaya_Alina/lab-01/diagrams/sequence-happy.png differ diff --git a/students/Kulikovskaya_Alina/lab-01/diagrams/sequence-happy.puml b/students/Kulikovskaya_Alina/lab-01/diagrams/sequence-happy.puml new file mode 100644 index 00000000..55e901e3 --- /dev/null +++ b/students/Kulikovskaya_Alina/lab-01/diagrams/sequence-happy.puml @@ -0,0 +1,104 @@ +@startuml +!theme plain +skinparam sequenceMessageAlign center +skinparam responseMessageBelowArrow true + +title Бронирование площадки - Happy Path (Online + Оплата) + +actor "Пользователь" as User +participant "Web UI\n(React/Vue)" as UI +participant "Booking API\n" as API +participant "Schedule Service" as Schedule +database "PostgreSQL" as DB +participant "Payment Gateway\n" as Payment +participant "Notification Service\n(Email/SMS)" as Notify + +User -> UI: 1. Открыть каталог площадок +activate UI +UI -> API: GET /api/courts?type=badminton +activate API +API -> DB: SELECT courts WHERE type='badminton' +activate DB +DB --> API: Список 8 кортов +deactivate DB +API --> UI: 200 OK + данные площадок +deactivate API +UI --> User: Отображение карточек кортов +deactivate UI + +User -> UI: 2. Выбрать корт #3, дату 15.03, время 18:00 +activate UI +UI -> API: GET /api/slots?court_id=3&date=2025-03-15 +activate API +API -> Schedule: Проверка доступности +activate Schedule +Schedule --> API: Доступные время [18:00, 19:00, 20:00] +deactivate Schedule +API --> UI: 200 OK + время +deactivate API +UI --> User: Отображение календаря с временем +deactivate UI + +User -> UI: 3. Нажать "Забронировать с оплатой" +activate UI +UI -> API: POST /api/bookings\n{court_id:3, slot:"18:00-19:00", date:"2025-03-15"} +activate API + +API -> Schedule: Блокировка времени (10 мин) +activate Schedule +Schedule --> API: OK (slot reserved) +deactivate Schedule + +API -> DB: INSERT booking\nstatus: PENDING_PAYMENT +activate DB +DB --> API: booking_id: "BK-2025-00142" +deactivate DB + +API --> UI: 201 Created + booking_id + payment_url +deactivate API +UI --> User: Редирект на страницу оплаты +deactivate UI + +User -> UI: 4. Ввести данные карты, подтвердить +activate UI +UI -> Payment: Charge(amount: 17BYN, booking_id: BK-2025-00142) +activate Payment +Payment -> Payment: Обработка платежа +Payment --> UI: 200 OK + payment_id: "PAY-789" +deactivate Payment + +UI -> API: POST /api/bookings/BK-2025-00142/confirm\n{payment_id: PAY-789} +activate API + +API -> Payment: Verify payment PAY-789 +activate Payment +Payment --> API: Status: SUCCESS +deactivate Payment + +API -> DB: UPDATE booking\nSET status='CONFIRMED', payment_id='PAY-789' +activate DB +DB --> API: OK +deactivate DB + +API -> Schedule: Подтверждение бронирования слота +activate Schedule +Schedule --> API: OK (slot confirmed) +deactivate Schedule + +API -> Notify: Отправить подтверждение\nbooking_id: BK-2025-00142 +activate Notify +Notify -> Notify: Генерация QR-кода +Notify --> API: OK (email queued) +deactivate Notify + +API --> UI: 200 OK + booking details +deactivate API +UI --> User: Страница успеха с QR-кодом +deactivate UI + +Notify -> User: Email с QR-кодом и деталями +activate Notify #LightBlue +Notify --> User: "Ваше бронирование подтверждено!" +deactivate Notify + +@enduml diff --git a/students/Kulikovskaya_Alina/lab-01/scenarios.feature b/students/Kulikovskaya_Alina/lab-01/scenarios.feature new file mode 100644 index 00000000..5ba709fa --- /dev/null +++ b/students/Kulikovskaya_Alina/lab-01/scenarios.feature @@ -0,0 +1,141 @@ +Feature: Бронирование спортивных площадок в манеже + + Как спортсмен или администратор + Я хочу бронировать площадки для игры + Чтобы гарантировать доступ к спортивному инвентарю в удобное время + + Background: + Given манеж работает с 08:00 до 23:00 + And в системе зарегистрированы площадки: + | тип | количество | часовая ставка | + | волейбол | 1 | 25 BYN | + | баскетбол | 1 | 25 BYN | + | бадминтон | 8 | 17 BYN | + | настольный теннис | 6 | 4 BYN | + + Scenario: Успешное online-бронирование с оплатой бадминтонного корта + Given пользователь "Александр" авторизован в системе + And бадминтонный корт #3 свободен на 15.03.2025 с 18:00 до 19:00 + And текущее время 14:00 (более чем за 4 часа до брони) + + When Александр выбирает тип площадки "Бадминтон" + And выбирает дату "15.03.2025" + And выбирает временной слот "18:00-19:00" + And нажимает "Забронировать с оплатой" + And вводит данные карты и подтверждает оплату 25 BYN + And платёжный шлюз возвращает статус "SUCCESS" + + Then система создаёт бронирование с ID "BK-2025-00142" + And статус бронирования "CONFIRMED" + And корт #3 на 18:00-19:00 помечен как "ЗАНЯТ" + And пользователь видит страницу успеха с QR-кодом + And система отправляет email с подтверждением на адрес пользователя + And в логах фиксируется событие "BookingConfirmed" + + Scenario: Бронирование без online-оплаты (оплата на месте) + Given пользователь "Мария" авторизована в системе + And волейбольная площадка свободна на 16.03.2025 с 10:00 до 11:00 + + When Мария выбирает волейбольную площадку + And выбирает слот "10:00-11:00" + And нажимает "Забронировать (оплата на месте)" + + Then система создаёт бронирование со статусом "RESERVED" + And слот временно заблокирован (grace period 30 минут до начала) + And пользователь видит сообщение "Оплатите 25 BYN на ресепшене за 30 минут до начала" + + Scenario: Бронирование администратором по телефону + Given администратор "Иван" авторизован с ролью "ADMIN" + And баскетбольная площадка свободна на сегодня с 20:00 до 21:00 + + When Иван открывает панель администратора + And выбирает баскетбольную площадку, слот 20:00-21:00 + And вводит данные клиента: ФИО "Петров Петр", телефон "+375291234567" + And нажимает "Создать бронирование" + + Then система создаёт бронирование со статусом "CONFIRMED" + And клиент получает SMS с подтверждением + And бронирование не требует online-оплаты + +# с ошибками + Scenario: Ошибка - слот уже занят другим пользователем (race condition) + Given пользователь "Александр" авторизован + And пользователь "Борис" авторизован + And бадминтонный корт #5 свободен на 15.03.2025 с 19:00 до 20:00 + + When Александр и Борис одновременно выбирают корт #5 на 19:00-20:00 + And Александр первым нажимает "Забронировать" + And система успешно создаёт бронирование для Александра + And Борис нажимает "Забронировать" через 2 секунды + + Then система показывает Борису ошибку "Этот слот только что заняли" + And предлагает альтернативные слоты: "18:00-19:00", "20:00-21:00", "21:00-22:00" + And бронирование для Бориса НЕ создаётся + + Scenario: Ошибка - таймаут платёжного шлюза + Given пользователь "Елена" авторизована + And стол для настольного тенниса #2 свободен на 15.03.2025 с 15:00 до 16:00 + + When Елена выбирает стол #2, слот 15:00-16:00 + And нажимает "Забронировать с оплатой" + And вводит данные карты + And платёжный шлюз не отвечает в течение 30 секунд (timeout) + + Then система НЕ отменяет бронирование сразу + And переводит статус в "PAYMENT_PENDING" + And показывает сообщение "Проверяем статус платежа..." + And запускает фоновую проверку через 1, 5 и 15 минут + And если через 15 минут платёж не подтверждён: + * статус меняется на "CANCELLED" + * слот освобождается + * пользователь получает уведомление "Бронирование отменено" + + Scenario: Ошибка - недостаточно средств на карте + Given пользователь "Дмитрий" авторизован + And бадминтонный корт #1 свободен на сегодня с 21:00 до 22:00 + + When Дмитрий выбирает корт #1, слот 21:00-22:00 + And нажимает "Забронировать с оплатой" + And вводит данные карты с недостаточным балансом + And платёжный шлюз возвращает ошибку "Insufficient funds" + + Then система сохраняет бронирование со статусом "PAYMENT_FAILED" + And показывает варианты: + | действие | + | Попробовать другую карту | + | Забронировать без оплаты (на месте) | + | Отменить бронирование | + And слот остаётся заблокированным на 10 минут для повторной попытки + And если действий нет → автоматическая отмена через 10 минут + + Scenario: Ошибка - слишком позднее бронирование (менее 30 минут до начала) + Given пользователь "Сергей" авторизован + And текущее время 17:45 + And бадминтонный корт #2 свободен с 18:00 до 19:00 (через 15 минут) + + When Сергей пытается забронировать корт #2 на 18:00-19:00 + + Then система возвращает ошибку "Слишком поздно для online-бронирования" + And показывает сообщение: "Звоните администратору: +375 (17) 123-45-67" + And бронирование НЕ создаётся + + Scenario: Ошибка - попытка отмены менее чем за 2 часа до начала + Given пользователь "Анна" имеет подтверждённое бронирование BK-2025-00099 + And бронирование на 15.03.2025 с 19:00 до 20:00 + And текущее время 17:30 (за 1.5 часа до начала) + + When Анна пытается отменить бронирование BK-2025-00099 + + Then система возвращает ошибку "Отмена возможна не позднее чем за 2 часа" + And показывает правила отмены + And бронирование остаётся активным + + Scenario: Ошибка - недоступен Schedule Service + Given пользователь "Ольга" авторизована + And Schedule Service недоступен (500 Internal Server Error) + + When Ольга открывает страницу выбора слотов + + Then система показывает ошибку "Сервис временно недоступен" + And предлагает позвонить администратору + And логирует ошибку для мониторинга diff --git a/students/Kulikovskaya_Alina/lab-01/use-case.md b/students/Kulikovskaya_Alina/lab-01/use-case.md new file mode 100644 index 00000000..56ec2286 --- /dev/null +++ b/students/Kulikovskaya_Alina/lab-01/use-case.md @@ -0,0 +1,196 @@ +# Use-case: Бронирование спортивной площадки в манеже + +**Предметная область:** Бронь манежа "Свободна площадка?" 🏸🏀🏐🏓 + +**Первичный актор:** Зарегистрированный пользователь (спортсмен/команда) или Администратор манежа + +**Вторичные акторы:** +- Payment Gateway (Платёжный шлюз) — обработка онлайн-оплаты +- Notification Service (Сервис уведомлений) — email/SMS подтверждения +- Database (База данных) — хранение бронирований +- Schedule Service (Сервис расписания) — проверка доступности слотов + +**Цель:** Забронировать спортивную площадку/корт/стол на конкретный временной слот (1 час) с возможностью онлайн-оплаты + +--- + +## Предусловия + +1. Пользователь аутентифицирован в системе (для online-бронирования) +2. Администратор авторизован с ролью "ADMIN" (для phone-бронирования) +3. Выбранная площадка существует в системе: + - Волейбольная площадка (1 шт.) + - Баскетбольная площадка (1 шт.) + - Бадминтонные корты (8 шт.) + - Столы для настольного тенниса (6 шт.) +4. Сервис расписания доступен для проверки слотов +5. Платёжный шлюз доступен (для online-оплаты) + +--- + +## Основной поток (Happy Path) — Online бронирование с оплатой + +1. Пользователь открывает каталог площадок на сайте +2. Система отображает список доступных типов площадок с фото и характеристиками +3. Пользователь выбирает тип: "Бадминтонный корт" +4. Система показывает календарь доступных дат и временных слотов (1 час) +5. Пользователь выбирает дату: "2025-03-15" и время: "18:00-19:00" +6. Система проверяет доступность через Schedule Service +7. Система отображает выбранный слот и стоимость (например, 25 BYN) +8. Пользователь нажимает "Забронировать с оплатой" +9. Система создаёт предварительное бронирование со статусом "PENDING_PAYMENT" (блокировка слота на 10 минут) +10. Система перенаправляет пользователя на страницу оплаты +11. Пользователь вводит данные карты и подтверждает оплату +12. Payment Gateway обрабатывает платёж и возвращает статус "SUCCESS" +13. Система обновляет бронирование: статус "CONFIRMED", фиксирует payment_id +14. Система сохраняет бронирование в Database +15. Система публикует событие "BookingConfirmed" +16. Notification Service отправляет email с QR-кодом подтверждения +17. Система отображает пользователю страницу успеха с деталями бронирования +18. Пользователь получает SMS-напоминание за 1 час до начала (опционально) + +**Постусловия:** +- Бронирование создано в БД со статусом "CONFIRMED" +- Временной слот 18:00-19:00 на корте #3 помечен как "ЗАНЯТ" +- Пользователь получил подтверждение на email +- Платёж зарегистрирован в системе + +--- + +## Альтернативные потоки + +### A1. Бронирование без оплаты (только резервирование) + +**Условие:** Пользователь выбирает "Забронировать без оплаты" (оплата на месте) + +**Поток (шаги 8-17 заменяются):** +8a. Пользователь нажимает "Забронировать (оплата на месте)"; +9a. Система создаёт бронирование со статусом "RESERVED" (без блокировки слота); +10a. Система устанавливает дедлайн оплаты: 30 минут до начала слота; +11a. Система сохраняет бронирование в Database; +12a. Notification Service отправляет email с напоминанием об оплате; +13a. Система отображает страницу успеха с инструкцией: "Оплатите на ресепшене за 30 минут до начала". + +**Постусловия:** +- Бронирование со статусом "RESERVED" +- Слот НЕ блокируется полностью (доступен для других до момента подтверждения) + +--- + +### A2. Бронирование администратором по телефону + +**Условие:** Клиент звонит администратору, нет online-доступа + +**Поток:** +1b. Администратор открывает панель управления; +2b. Выбирает тип площадки и просматривает доступные слоты; +3b. Клиент диктует желаемое время по телефону; +4b. Администратор выбирает слот и нажимает "Забронировать (админ)"; +5b. Система запрашивает ФИО клиента и телефон; +6b. Администратор вводит данные клиента; +7b. Система создаёт бронирование со статусом "CONFIRMED" (без online-оплаты); +8b. Система отправляет SMS клиенту с подтверждением; +9b. Администратор видит подтверждение создания брони; + +**Постусловия:** +- Бронирование создано администратором +- Оплата производится на ресепшене +- Клиент получает SMS + +--- + +### A3. Отмена бронирования пользователем + +**Условие:** Пользователь отменяет бронь до начала слота + +**Поток:** +1c. Пользователь открывает "Мои бронирования"; +2c. Выбирает активное бронирование и нажимает "Отменить"; +3c. Система проверяет: отмена возможна не позднее чем за 2 часа до начала; +4c. Система меняет статус на "CANCELLED"; +5c. Если была online-оплата → инициируется возврат средств; +6c. Слот освобождается в расписании; +7c. Система отправляет подтверждение отмены на email; +8c. Пользователь видит сообщение "Бронирование отменено". + +--- + +## Исключительные ситуации + +### E1. Слот уже занят (Race Condition) + +**Условие:** Два пользователя одновременно пытаются забронировать последний слот + +**Поток:** +1d. Пользователь А и Пользователь Б выбирают один слот 18:00-19:00; +2d. Пользователь А нажимает "Забронировать" первым; +3d. Система создаёт бронирование А со статусом "PENDING_PAYMENT", блокирует слот; +4d. Пользователь Б нажимает "Забронировать"; +5d. Schedule Service возвращает ошибку: "Слот уже забронирован"; +6d. Система показывает Б сообщение: "Извините, этот слот только что заняли. Выберите другое время."; +7d. Система предлагает альтернативные слоты на тот же день; +8d. Use-case для Б завершается неудачей (но можно начать заново). + +--- + +### E2. Таймаут платёжного шлюза + +**Условие:** Payment Gateway не отвечает в течение 30 секунд + +**Поток:** +1e. Пользователь подтверждает оплату (шаг 11 основного потока); +2e. Система отправляет запрос в Payment Gateway; +3e. Через 30 секунд нет ответа (TimeoutException); +4e. Система НЕ откатывает бронирование сразу; +5e. Система переводит бронирование в статус "PAYMENT_PENDING"; +6e. Система запускает фоновую задачу проверки статуса платежа (через 1, 5, 15 минут); +7e. Пользователь видит: "Обработка платежа... Пожалуйста, не закрывайте страницу"; +8e. Если через 15 минут платёж не подтверждён: + - Система отменяет бронирование; + - Освобождает слот; + - Уведомляет пользователя: "Платёж не прошёл. Попробуйте снова.". + +--- + +### E3. Недостаточно средств на карте + +**Условие:** Payment Gateway возвращает ошибку "Insufficient funds" + +**Поток:** +1f. Пользователь подтверждает оплату; +2f. Payment Gateway возвращает ошибку: "Недостаточно средств"; +3f. Система сохраняет бронирование со статусом "PAYMENT_FAILED"; +4f. Система предлагает пользователю: + - Попробовать другую карту; + - Перейти к бронированию без оплаты (оплата на месте); + - Отменить бронирование; +5f. Слот остаётся заблокированным ещё 10 минут для повторной попытки; +6f. Если пользователь не действует → автоматическая отмена через 10 минут. + +--- + +### E4. Нарушение бизнес-правила (минимальное время до брони) + +**Условие:** Пользователь пытается забронировать слот, который начинается менее чем через 30 минут + +**Поток:** +1g. Пользователь выбирает слот 14:00-15:00, текущее время 13:45; +2g. Система проверяет правило: "Бронирование возможно не позднее чем за 30 минут до начала"; +3g. Система возвращает ошибку: "Слишком поздно для online-бронирования. Звоните администратору: +375XX XXX-XX-XX"; +4g. Система предлагает контакт телефона администратора; +5g. Use-case завершается неудачей. + +--- + +### E5. Service Unavailable (Schedule Service недоступен) + +**Условие:** Schedule Service не отвечает при проверке доступности + +**Поток:** +1h. Пользователь выбирает дату и время; +2h. Система отправляет запрос в Schedule Service; +3h. Connection refused / Timeout; +4h. Система возвращает ошибку 503; +5h. Система показывает пользователю: "Сервис временно недоступен. Попробуйте обновить страницу или позвоните администратору."; +6h. Система логирует ошибку для мониторинга; +7h. Use-case завершается неудачей. \ No newline at end of file diff --git "a/students/Kulikovskaya_Alina/lab-01/\320\236\321\202\321\207\320\265\321\202.md" "b/students/Kulikovskaya_Alina/lab-01/\320\236\321\202\321\207\320\265\321\202.md" new file mode 100644 index 00000000..a6c43d01 --- /dev/null +++ "b/students/Kulikovskaya_Alina/lab-01/\320\236\321\202\321\207\320\265\321\202.md" @@ -0,0 +1,287 @@ +

Министерство образования Республики Беларусь

+

Учреждение образования

+

"Брестский Государственный технический университет"

+

Кафедра ИИТ

+





+

Лабораторная работа №1

+

По дисциплине: "Проектирование интернет-систем"

+

Тема: "Сценарий транзакции: моделирование use-case и границ ответственности"

+





+

Выполнил:

+

Студент 3 курса

+

Группы ПО-13

+

Куликовская А. В.

+

Проверил:

+

Шорох Д.В.

+




+

Брест 2026

+ +--- + +## Цель работы + +Научиться анализировать бизнес-процессы интернет-системы, выявлять границы ответственности компонентов и моделировать транзакционные сценарии с учётом возможных сбоев. + +--- + +## Вариант №51 - Бронь манежа "Свободна площадка?" 🏸🏀🏐🏓 + +**Питч:** забронируй нужную площадку для игры. + +**Ядро домена:** Площадки, Расписание, Брони, Отмены + +--- + +## Ход выполнения работы + +### 1. Структура проекта + +``` +lab-01/ +├── README.md # Основной отчёт (этот документ) +├── use-case.md # Текстовое описание use-case +├── diagrams/ +│ ├── sequence-happy.puml # PlantUML для успешного сценария +│ └── sequence-error-payment.puml +├── scenarios.feature # Gherkin-сценарии +└── analysis.md # Анализ границ ответственности +``` + +--- + +### 2. Use-case описание + +👉 **Ссылка на файл:** [use-case.md](https://github.com/skumbriya21/PIS-2026/blob/lab01-po13-kulikovskaya/students/Kulikovskaya_Alina/lab-01/use-case.md) + +**Основной сценарий:** Забронировать площадку на нужное время + +**Первичный актор:** Зарегистрированный пользователь (спортсмен/команда) или Администратор манежа + +**Цель:** Забронировать спортивную площадку/корт/стол на конкретный временной слот (1 час) с возможностью онлайн-оплаты + +**Краткое описание основного потока:** +1. Пользователь открывает сайт с каталогом площадок. +2. Система отображает список доступных типов площадок с фото и характеристиками. +3. Пользователь выбирает тип: "Бадминтонный корт". +4. Система показывает календарь доступных дат и временных слотов (1 час). +5. Пользователь выбирает дату: "2025-03-15" и время: "18:00-19:00". +6. Система проверяет доступность выбранного слота. +7. Система отображает выбранный слот и стоимость. +8. Пользователь нажимает "Забронировать с оплатой" и оплачивает. +9. Пользователь получает SMS-напоминание за 1 час до начала. + +**Альтернативные потоки:** + - Пользователь хочет забранировать площадку, но не оплачивать. + - Пользователь не хочет регистрироваться на сайте и звонит администратору для брони. + - Отмена бронирования пользователем. + +**Исключительные ситуации:** + - Временной слот уже занят. + - Таймаут платёжного шлюза. + - Недостаточно средств на карте. + - Нарушение бизнес-правила. + +--- + +### 3. Диаграммы последовательности (Sequence Diagrams) + +#### 3.1. Happy Path (успешный сценарий) + +👉 **PlantUML исходник:** [sequence-happy.puml](diagrams/sequence-happy.puml) + +![Диаграмма успешного сценария](diagrams/sequence-happy.png) + +**Описание потока:** +- Пользователь видит список площадок в веб интерфейсе. +- Пользователь бронирует площадку. +- Создается бронь. +- Бронь оплачивается и подтверждается. +- Отправляется уведомление по SMS/Email. +- Пользователю показывается успешная бронь. + +**Участники:** +- Авторизованый пользователь. +- Ui. +- API. +- NotificationService. +- DataBase. +- PaymentGateway. + +#### 3.2. Error Case (сценарий с ошибкой) + +👉 **PlantUML исходник:** [sequence-error-payment.puml](diagrams/sequence-error-payment.puml) + +![Диаграмма сценария с ошибкой](diagrams/sequence-error-payment.png) + +**Описание потока:** +- Ошибка: таймаут платежного шлюза (долго грузилась оплата). + +--- + +### 4. Gherkin-сценарии + +👉 **Ссылка на файл:** [scenarios.feature](scenarios.feature) + +**Реализовано сценариев:** 7 + +**Список сценариев:** +1. ✅ **Успешный сценарий** (Happy Path) +2. ✅ **Успешный сценарий** Бронирование администратором по телефону +3. ✅ **Успешный сценарий** Бронирование без online-оплаты (оплата на месте) +4. ✅ **Ошибка:** Cлот уже занят другим пользователем +5. ✅ **Ошибка:** Таймаут сервиса оплаты +6. ✅ **Ошибка:** Недостаточно средств на карте +7. ✅ **Ошибка:** слишком позднее бронирование (менее 30 минут до начала) +8. ✅ **Ошибка:**недоступен Schedule Service +9. ✅ **Ошибка:**попытка отмены менее чем за 2 часа до начала + +**Пример сценария:** +```gherkin +Feature: Бронирование спортплощадки + + Scenario: Бронирование администратором по телефону + Given администратор "Иван" авторизован с ролью "ADMIN" + And баскетбольная площадка свободна на сегодня с 20:00 до 21:00 + + When Иван открывает панель администратора + And выбирает баскетбольную площадку, слот 20:00-21:00 + And вводит данные клиента: ФИО "Петров Петр", телефон "+375291234567" + And нажимает "Создать бронирование" + + Then система создаёт бронирование со статусом "CONFIRMED" + And клиент получает SMS с подтверждением + And бронирование не требует online-оплаты +``` + +--- + +### 5. Анализ границ ответственности + +👉 **Ссылка на файл:** [analysis.md](analysis.md) + +#### 5.1. Транзакционные границы + +| Операция | Тип | Откат при ошибке | Retry-стратегия | Идемпотентность | +|----------|-----|------------------|-----------------|-----------------| +| **Блокировка слота** | Синхронная | Да (release lock) | Нет (TTL 10 минут) | Да (idempotency_key = booking_id) | +| **Создание бронирования в БД** | Синхронная | Да (DELETE) | Нет (PK constraint) | Да (проверка уникальности user+slot+date) | +| **Вызов Payment Gateway** | Синхронная | Нет (async проверка) | 3 попытки с exponential backoff (1s, 2s, 4s) | Да (payment_intent_id) | +| **Обновление статуса бронирования** | Синхронная | Да (ROLLBACK) | Нет | Да | +| **Отправка email** | **Асинхронная** | Нет (best-effort) | 5 попыток с exp. backoff | Да (deduplication по booking_id) | +| **Освобождение слота при отмене** | Синхронная | Нет | Нет | Да | + +#### 5.2. Обработка исключительных ситуаций + +**Реализовано стратегий обработки:** 4 + +**Примеры:** + +### 4.1 Race Condition (двойное бронирование) + +**Условие:** Два пользователя одновременно бронируют последний слот + +**Обнаружение:** +- Schedule Service возвращает ошибку "Slot already locked" +- Или БД возвращает constraint violation при INSERT + +**Реакция:** +1. Транзакция второго пользователя откатывается +2. Система предлагает альтернативные слоты +3. Логируется попытка конфликтного доступа + +**Компенсация:** +- Не требуется (атомарная проверка в Schedule Service) +- Слот остаётся заблокированным за первым пользователем + +**Уведомление:** +- "Извините, этот слот только что заняли. Выберите другое время: 19:00-20:00 или 20:00-21:00" + + +### 4.2 Нарушение бизнес-правила (минимальное время) + +**Условие:** Попытка бронирования < 30 минут до начала слота + +**Обнаружение:** +- Валидация на уровне Application Layer: `if (slot_start - now) < 30min` + +**Реакция:** +1. Отклонить запрос до создания бронирования +2. Предложить позвонить администратору + +**Компенсация:** +- Не требуется (бронирование не создавалось) + +**Уведомление:** +- "Слишком поздно для online-бронирования. Звоните: +375 (17) 123-45-67" + +--- + +## Таблица критериев оценки + +| Критерий | Баллы | Выполнено | +|----------|-------|-----------| +| Use-case описание (полнота: акторы, предусловия, основной поток, альтернативы, исключения) | 15 | ✅ | +| Sequence diagram (happy path) - корректность нотации UML, включение всех ключевых компонентов | 20 | ✅ | +| Sequence diagram (error case) - моделирование хотя бы одной исключительной ситуации | 15 | ✅ | +| Gherkin-сценарии - минимум 4 сценария (1 успешный + 3 ошибочных) | 20 | ✅ | +| Анализ границ ответственности - таблица транзакционных границ, обоснование выбора синхронных/асинхронных операций | 15 | ✅ | +| Обработка исключений - описание стратегий retry, компенсации, уведомлений | 10 | ✅ | +| Качество документации - оформление, читаемость, грамотность | 5 | ✅ | +| **ИТОГО** | **100** | | + +--- + +## Контрольные вопросы + +**Подготовка к защите:** + +1. Что такое транзакционная граница? Где она проходит в вашем сценарии? + - Транзакционная граница — это участок процесса, где операции выполняются атомарно и согласованно. + - В нашем сценарии она начинается при нажатии пользователем кнопки «Забронировать» и заканчивается фиксацией брони в базе данных и подтверждением оплаты. + +2. Почему операция X выбрана синхронной, а Y - асинхронной? + - Синхронные операции (создание брони, проверка слота, вызов платёжного сервиса) критичны для целостности данных и требуют немедленного результата. + - Асинхронные операции (отправка email, уведомления) не влияют на факт брони и могут выполняться позже без нарушения логики. + +3. Как обеспечить идемпотентность при повторных запросах? + - Использовать уникальные идентификаторы операций (idempotency key). + - Проверять статус уже выполненной операции перед созданием новой. + - При повторном запросе возвращать результат существующей операции вместо дублирования. + +4. Что произойдёт, если внешний сервис вернёт ошибку после частичного выполнения операции? + - Система переводит процесс в промежуточный статус. + - Запускается механизм компенсации: откат изменений или отмена операции. + - Пользователь получает уведомление о задержке или сбое. + +5. Как система обнаружит, что внешний сервис недоступен? + - По таймауту сетевого запроса или по коду ошибки (например, 503 Service Unavailable). + - Событие фиксируется в логах. + - Запускается стратегия повторных попыток или постановка задачи в очередь. + +6. Какие данные нужно логировать для диагностики сбоев? + - Уникальный идентификатор операции. + - Пользовательский контекст (например, ID пользователя). + - Тип операции и её параметры. + - Время и причина ошибки (timeout, отказ сервиса). + - Количество попыток повторного выполнения и их результат. + - Текущий статус операции. + +--- + +## Ссылка на репозиторий + +👉 **GitHub:** [репозиторий](https://github.com/skumbriya21/PIS-2026) + +--- + +## Вывод + +> В ходе выполнения лабораторной работы был проанализирован бизнес-процесс "Бронь манежа "Свободна площадка?" 🏸🏀🏐🏓". Разработаны use-case диаграммы для основного сценария и альтернативных потоков. Построены sequence diagrams с использованием PlantUML для визуализации взаимодействия компонентов системы. Созданы Gherkin-сценарии для автоматизированного тестирования. Определены транзакционные границы и стратегии обработки ошибок. Освоены навыки моделирования распределённых транзакций и анализа точек отказа в интернет-системах. + +--- + +**Дата выполнения:** 12.03.2026 + +**Оценка:** _____________ + +**Подпись преподавателя:** _____________ diff --git a/students/Kulikovskaya_Alina/lab-02/Architecture.md b/students/Kulikovskaya_Alina/lab-02/Architecture.md new file mode 100644 index 00000000..c0e05c17 --- /dev/null +++ b/students/Kulikovskaya_Alina/lab-02/Architecture.md @@ -0,0 +1,166 @@ +# Архитектурная диаграмма + +## Диаграмма слоёв +``` +┌───────────────────────────────────────────────────────────────────────────────┐ +│ ВНЕШНИЙ МИР │ +│ ┌─────────────┐ ┌─────────────┐ ┌───────────────┐ ┌─────────────────────┐ │ +│ │ Web UI │ │ Admin Panel│ │Payment Gateway│ │Notification Service │ │ +│ │ (React) │ │ (React) │ │ (YooKassa) │ │ (Email/SMS) │ │ +│ └──────┬──────┘ └──────┬──────┘ └───────┬───────┘ └──────────┬──────────┘ │ +└─────────┼────────────────┼─────────────────┼─────────────────────┼────────────┘ + │ │ │ │ + ▼ ▼ ▼ ▼ +┌───────────────────────────────────────────────────────────────────────────────┐ +│ INFRASTRUCTURE LAYER (Адаптеры) │ +│ │ +│ ┌─────────────────────────────────┐ ┌─────────────────────────────────┐ │ +│ │ ВХОДЯЩИЕ АДАПТЕРЫ │ │ ИСХОДЯЩИЕ АДАПТЕРЫ │ │ +│ │ ┌─────────────────────────┐ │ │ ┌─────────────────────────┐ │ │ +│ │ │ BookingController │◄───┼────┼──┤ BookingRepository │ │ │ +│ │ │ (REST API) │ │ │ │ (PostgreSQL) │ │ │ +│ │ └─────────────────────────┘ │ │ └─────────────────────────┘ │ │ +│ │ ┌─────────────────────────┐ │ │ ┌─────────────────────────┐ │ │ +│ │ │ AdminController │◄───┼────┼──┤ CourtRepository │ │ │ +│ │ │ (REST API) │ │ │ │ (PostgreSQL) │ │ │ +│ │ └─────────────────────────┘ │ │ └─────────────────────────┘ │ │ +│ │ ┌─────────────────────────┐ │ │ ┌─────────────────────────┐ │ │ +│ │ │ PaymentWebhookController│◄───┼────┼──┤ ScheduleRepository │ │ │ +│ │ │ (REST API) │ │ │ │ (Redis/PostgreSQL) │ │ │ +│ │ └─────────────────────────┘ │ │ └─────────────────────────┘ │ │ +│ │ │ │ ┌─────────────────────────┐ │ │ +│ │ │ │ │ PaymentGatewayClient │───►│ │ +│ │ │ │ │ (HTTP Client) │ │ │ +│ │ │ │ └─────────────────────────┘ │ │ +│ │ │ │ ┌─────────────────────────┐ │ │ +│ │ │ │ │ NotificationClient │───►│ │ +│ │ │ │ │ (HTTP Client) │ │ │ +│ │ │ │ └─────────────────────────┘ │ │ +│ └─────────────────────────────────┘ └─────────────────────────────────┘ │ +│ ▲ ▲ │ +│ │ │ │ +│ └───────────────────┬───────────────┘ │ +│ │ │ +└───────────────────────────────────────────────────┼───────────────────────────┘ + │ + ▼ +┌────────────────────────────────────────────────────────────────────────────────────────┐ +│ APPLICATION LAYER (Порты) │ +│ │ +│ ┌───────────────────────────────────────┐ ┌──────────────────────────────────────┐ │ +│ │ ВХОДЯЩИЕ ПОРТЫ (Интерфейсы) │ │ ИСХОДЯЩИЕ ПОРТЫ (Интерфейсы) │ │ +│ │ │ │ │ │ +│ │ interface IBookingService { │ │ interface IBookingRepository { │ │ +│ │ createBooking(cmd): BookingId │ │ save(booking): void │ │ +│ │ cancelBooking(id): void │ │ findById(id): Booking │ │ +│ │ getBooking(id): BookingDTO │ │ findByUser(userId): List │ │ +│ │ listAvailableSlots(): List │ │ findActiveByCourt(court, │ │ +│ │ } │ │ date) │ │ +│ │ │ │ } │ │ +│ │ interface IAdminService { │ │ │ │ +│ │ createPhoneBooking(cmd): BookingId │ interface ICourtRepository { │ │ +│ │ confirmPayment(id): void │ │ findById(id): Court │ │ +│ │ } │ │ findByType(type): List │ │ +│ │ │ │ findAll(): List │ │ +│ │ interface IPaymentService { │ │ } │ │ +│ │ processPayment(cmd): Result │ │ │ │ +│ │ verifyPayment(id): Status │ │ interface IScheduleRepository{ │ │ +│ │ } │ │ isAvailable(court, slot): bool │ │ +│ │ │ │ lockSlot(court, slot): bool │ │ +│ │ │ │ unlockSlot(court, slot): void │ │ +│ │ │ │ confirmSlot(court, slot): void │ │ +│ │ │ │ } │ │ +│ │ │ │ │ │ +│ │ │ │ interface IPaymentGateway { │ │ +│ │ │ │ charge(amount, currency): Result │ │ +│ │ │ │ refund(paymentId): Result │ │ +│ │ │ │ getStatus(paymentId): Status │ │ +│ │ │ │ } │ │ +│ │ │ │ │ │ +│ │ │ │ interface INotificationService{ │ │ +│ │ │ │ sendBookingConfirmation(to, │ │ +│ │ │ │ booking)│ │ +│ │ │ │ sendReminder(to, booking): void │ │ +│ │ │ │ } │ │ +│ └───────────────────────────────────────┘ └──────────────────────────────────────┘ │ +│ ▲ ▲ │ +│ │ │ │ +│ └─────────────────┬──────────────┘ │ +│ │ │ +└───────────────────────────────────────────────────┼────────────────────────────────────┘ + │ + ▼ +┌──────────────────────────────────────────────────────────────────────────────┐ +│ DOMAIN LAYER (Ядро) │ +│ │ +│ ┌──────────────────────────────────────────────────────────────────────────┐│ +│ │ Entities (Сущности) ││ +│ │ ┌─────────────┐ ┌─────────────┐ ┌──────────────┐ ┌─────────────────┐ ││ +│ │ │ Booking │ │ Court │ │ Slot │ │ Payment │ ││ +│ │ │ (Aggregate)│ │ (Entity) │ │(Value Object)│ │ (Entity) │ ││ +│ │ │ │ │ │ │ │ │ │ ││ +│ │ │ - id: UUID │ │ - id: UUID │ │ - start: Time│ │ - id: UUID │ ││ +│ │ │ - userId │ │ - name │ │ - end: Time │ │ - bookingId │ ││ +│ │ │ - courtId │ │ - type │ │ - date: Date │ │ - amount │ ││ +│ │ │ - slot │ │ - capacity │ │ │ │ - status │ ││ +│ │ │ - status │ │ - price │ │ │ │ - method │ ││ +│ │ │ - payment │ │ - isActive │ │ │ │ │ ││ +│ │ │ │ │ │ │ │ │ │ ││ +│ │ │ confirm() │ │ activate() │ │ overlaps()? │ │ refund() │ ││ +│ │ │ cancel() │ │ deactivate()│ │ │ │ confirm() │ ││ +│ │ │ isExpired() │ │ │ │ │ │ │ ││ +│ │ └─────────────┘ └─────────────┘ └──────────────┘ └─────────────────┘ ││ +│ │ ││ +│ │ ┌─────────────┐ ┌─────────────┐ ┌──────────────┐ ┌─────────────────┐ ││ +│ │ │ User │ │ CourtType │ │ BookingStatus│ │ PaymentStatus │ ││ +│ │ │ (Entity) │ │ (Enum/VO) │ │ (Enum) │ │ (Enum) │ ││ +│ │ │ │ │ │ │ │ │ │ ││ +│ │ │ - id │ │ VOLLEYBALL │ │ PENDING │ │ PENDING │ ││ +│ │ │ - email │ │ BASKETBALL │ │ RESERVED │ │ PROCESSING │ ││ +│ │ │ - phone │ │ BADMINTON │ │ CONFIRMED │ │ SUCCESS │ ││ +│ │ │ - role │ │ TABLE_TENNIS│ │ CANCELLED │ │ FAILED │ ││ +│ │ │ │ │ │ │ EXPIRED │ │ REFUNDED │ ││ +│ │ └─────────────┘ └─────────────┘ └──────────────┘ └─────────────────┘ ││ +│ │ ││ +│ └──────────────────────────────────────────────────────────────────────────┘│ +│ │ +│ ┌──────────────────────────────────────────────────────────────────────────┐│ +│ │ Domain Events (События) ││ +│ │ ││ +│ │ BookingCreatedEvent → После создания бронирования ││ +│ │ BookingConfirmedEvent → После подтверждения оплаты ││ +│ │ BookingCancelledEvent → После отмены ││ +│ │ PaymentReceivedEvent → После получения платежа ││ +│ │ SlotLockedEvent → После блокировки слота ││ +│ │ ││ +│ └──────────────────────────────────────────────────────────────────────────┘│ +│ │ +└──────────────────────────────────────────────────────────────────────────────┘ + +``` + + +--- + +## Описание портов и адаптеров + +| Тип | Название | Назначение | +| --- | --- | --- | +| **Входящий порт** | IBookingService | *Интерфейс для управления бронированиями клиентов (создание, отмена, просмотр)* | +| **Входящий порт** | IAdminService | *Интерфейс для административных операций (бронирование по телефону, подтверждение оплаты)* | +| **Входящий порт** | IPaymentService | *Интерфейс для обработки платежей (списание, возврат, проверка статуса)* | +| **Исходящий порт** | IBookingRepository | *Интерфейс для хранения и извлечения бронирований из БД* | +| **Исходящий порт** | ICourtRepository | *Интерфейс для доступа к данным спортивных площадок* | +| **Исходящий порт** | IScheduleRepository | *Интерфейс для управления расписанием, блокировки и проверки доступности слотов* | +| **Исходящий порт** | IPaymentGateway | *Интерфейс для интеграции с внешними платёжными системами (YooKassa, Stripe)* | +| **Исходящий порт** | INotificationService | *Интерфейс для отправки уведомлений пользователям (email, SMS, push)* | +| **Входящий адаптер** | BookingController (REST) | *REST API эндпоинты для операций бронирования (/api/bookings)* | +| **Входящий адаптер** | AdminController (REST) | *REST API для административной панели (/api/admin/bookings)* | +| **Входящий адаптер** | PaymentWebhookController (REST) | *Обработка вебхуков от платёжных систем (/api/webhooks/payment)* | +| **Исходящий адаптер** | InMemoryBookingRepository | *Реализация хранилища бронирований в памяти (для тестов и разработки)* | +| **Исходящий адаптер** | InMemoryCourtRepository | *Реализация хранилища площадок в памяти* | +| **Исходящий адаптер** | InMemoryScheduleRepository | *Реализация расписания и блокировок в памяти (имитация Redis)* | +| **Исходящий адаптер** | MockPaymentGateway | *Mock-реализация платёжного шлюза для тестирования* | +| **Исходящий адаптер** | MockNotificationService | *Mock-реализация сервиса уведомлений (логирование вместо отправки)* | + +--- diff --git a/students/Kulikovskaya_Alina/lab-02/lab2.png b/students/Kulikovskaya_Alina/lab-02/lab2.png new file mode 100644 index 00000000..e514d654 Binary files /dev/null and b/students/Kulikovskaya_Alina/lab-02/lab2.png differ diff --git a/students/Kulikovskaya_Alina/lab-02/src/__init__.py b/students/Kulikovskaya_Alina/lab-02/src/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/students/Kulikovskaya_Alina/lab-02/src/application/__init__.py b/students/Kulikovskaya_Alina/lab-02/src/application/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/students/Kulikovskaya_Alina/lab-02/src/application/commands/__init__.py b/students/Kulikovskaya_Alina/lab-02/src/application/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/students/Kulikovskaya_Alina/lab-02/src/application/commands/cancel_booking_command.py b/students/Kulikovskaya_Alina/lab-02/src/application/commands/cancel_booking_command.py new file mode 100644 index 00000000..e69de29b diff --git a/students/Kulikovskaya_Alina/lab-02/src/application/commands/confirm_payment_command.py b/students/Kulikovskaya_Alina/lab-02/src/application/commands/confirm_payment_command.py new file mode 100644 index 00000000..e69de29b diff --git a/students/Kulikovskaya_Alina/lab-02/src/application/commands/create_booking_command.py b/students/Kulikovskaya_Alina/lab-02/src/application/commands/create_booking_command.py new file mode 100644 index 00000000..e69de29b diff --git a/students/Kulikovskaya_Alina/lab-02/src/application/ports/__init__.py b/students/Kulikovskaya_Alina/lab-02/src/application/ports/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/students/Kulikovskaya_Alina/lab-02/src/domain/__init__.py b/students/Kulikovskaya_Alina/lab-02/src/domain/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/students/Kulikovskaya_Alina/lab-02/src/domain/events/__init__.py b/students/Kulikovskaya_Alina/lab-02/src/domain/events/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/students/Kulikovskaya_Alina/lab-02/src/domain/events/booking_cancelled.py b/students/Kulikovskaya_Alina/lab-02/src/domain/events/booking_cancelled.py new file mode 100644 index 00000000..e69de29b diff --git a/students/Kulikovskaya_Alina/lab-02/src/domain/events/booking_confirmed.py b/students/Kulikovskaya_Alina/lab-02/src/domain/events/booking_confirmed.py new file mode 100644 index 00000000..e69de29b diff --git a/students/Kulikovskaya_Alina/lab-02/src/domain/events/booking_created.py b/students/Kulikovskaya_Alina/lab-02/src/domain/events/booking_created.py new file mode 100644 index 00000000..e69de29b diff --git a/students/Kulikovskaya_Alina/lab-02/src/domain/events/domain_event.py b/students/Kulikovskaya_Alina/lab-02/src/domain/events/domain_event.py new file mode 100644 index 00000000..e69de29b diff --git a/students/Kulikovskaya_Alina/lab-02/src/domain/exceptions/__init__.py b/students/Kulikovskaya_Alina/lab-02/src/domain/exceptions/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/students/Kulikovskaya_Alina/lab-02/src/domain/exceptions/domain_exception.py b/students/Kulikovskaya_Alina/lab-02/src/domain/exceptions/domain_exception.py new file mode 100644 index 00000000..e69de29b diff --git a/students/Kulikovskaya_Alina/lab-02/src/domain/models/__init__.py b/students/Kulikovskaya_Alina/lab-02/src/domain/models/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/students/Kulikovskaya_Alina/lab-02/src/domain/models/booking.py b/students/Kulikovskaya_Alina/lab-02/src/domain/models/booking.py new file mode 100644 index 00000000..e69de29b diff --git a/students/Kulikovskaya_Alina/lab-02/src/domain/models/court.py b/students/Kulikovskaya_Alina/lab-02/src/domain/models/court.py new file mode 100644 index 00000000..e69de29b diff --git a/students/Kulikovskaya_Alina/lab-02/src/domain/models/payment.py b/students/Kulikovskaya_Alina/lab-02/src/domain/models/payment.py new file mode 100644 index 00000000..e69de29b diff --git a/students/Kulikovskaya_Alina/lab-02/src/domain/models/user.py b/students/Kulikovskaya_Alina/lab-02/src/domain/models/user.py new file mode 100644 index 00000000..e69de29b diff --git a/students/Kulikovskaya_Alina/lab-02/src/domain/models/value_objects/__init__.py b/students/Kulikovskaya_Alina/lab-02/src/domain/models/value_objects/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/students/Kulikovskaya_Alina/lab-02/src/domain/models/value_objects/booking_status.py b/students/Kulikovskaya_Alina/lab-02/src/domain/models/value_objects/booking_status.py new file mode 100644 index 00000000..e69de29b diff --git a/students/Kulikovskaya_Alina/lab-02/src/domain/models/value_objects/court_type.py b/students/Kulikovskaya_Alina/lab-02/src/domain/models/value_objects/court_type.py new file mode 100644 index 00000000..e69de29b diff --git a/students/Kulikovskaya_Alina/lab-02/src/domain/models/value_objects/money.py b/students/Kulikovskaya_Alina/lab-02/src/domain/models/value_objects/money.py new file mode 100644 index 00000000..e69de29b diff --git a/students/Kulikovskaya_Alina/lab-02/src/domain/models/value_objects/payment_status.py b/students/Kulikovskaya_Alina/lab-02/src/domain/models/value_objects/payment_status.py new file mode 100644 index 00000000..e69de29b diff --git a/students/Kulikovskaya_Alina/lab-02/src/domain/models/value_objects/slot.py b/students/Kulikovskaya_Alina/lab-02/src/domain/models/value_objects/slot.py new file mode 100644 index 00000000..e69de29b diff --git a/students/Kulikovskaya_Alina/lab-02/src/infrastructure/__init__.py b/students/Kulikovskaya_Alina/lab-02/src/infrastructure/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/students/Kulikovskaya_Alina/lab-02/src/infrastructure/adapters/__init__.py b/students/Kulikovskaya_Alina/lab-02/src/infrastructure/adapters/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/students/Kulikovskaya_Alina/lab-02/src/infrastructure/config/__init__.py b/students/Kulikovskaya_Alina/lab-02/src/infrastructure/config/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/students/Kulikovskaya_Alina/lab-02/src/infrastructure/config/dependency_injection.py b/students/Kulikovskaya_Alina/lab-02/src/infrastructure/config/dependency_injection.py new file mode 100644 index 00000000..e69de29b diff --git "a/students/Kulikovskaya_Alina/lab-02/\320\236\321\202\321\207\321\221\321\202.md" "b/students/Kulikovskaya_Alina/lab-02/\320\236\321\202\321\207\321\221\321\202.md" new file mode 100644 index 00000000..ba4122b7 --- /dev/null +++ "b/students/Kulikovskaya_Alina/lab-02/\320\236\321\202\321\207\321\221\321\202.md" @@ -0,0 +1,840 @@ +p align="center">Министерство образования Республики Беларусь

+

Учреждение образования

+

"Брестский Государственный технический университет"

+

Кафедра ИИТ

+





+

Лабораторная работа №2

+

По дисциплине: "Проектирование интернет-систем"

+

Тема: "Гексагональная архитектура: проектирование портов и адаптеров"

+





+

Выполнил:

+

Студент 3 курса

+

Группы ПО-13

+

Куликовская А. В.

+

Проверил:

+

Шорох Д.В.

+




+

Брест 2026

+ +--- + +## Цель работы + +Спроектировать архитектуру основного сервиса системы с использованием гексагональной (hexagonal) архитектуры: создать структуру проекта, определить порты (интерфейсы) и продемонстрировать изоляцию слоёв через минимальные примеры. + +--- + +## Вариант №51 - Бронь манежа "Свободна площадка?" 🏸🏀🏐🏓 + +**Питч:** забронируй нужную площадку для игры. + +**Ядро домена:** Площадки, Расписание, Брони, Отмены + +**Выбранный сервис:** Booking Service + +--- + +## Ход выполнения работы + +### Часть 1. Архитектурная диаграмма + +**Описание сервиса:** Booking Service управляет жизненным циклом бронирования спортивных площадок: поиск площадок, просмотр расписания, создание бронирований и управление отзывами пользователей.Основные сущности: Court (Площадка), Schedule (Расписание), Booking (Бронь), Review (Отзыв). + +**Диаграмма слоёв:** + +``` +┌───────────────────────────────────────────────────────────────────────────────┐ +│ ВНЕШНИЙ МИР │ +│ ┌─────────────┐ ┌─────────────┐ ┌───────────────┐ ┌─────────────────────┐ │ +│ │ Web UI │ │ Admin Panel│ │Payment Gateway│ │Notification Service │ │ +│ │ (React) │ │ (React) │ │ (YooKassa) │ │ (Email/SMS) │ │ +│ └──────┬──────┘ └──────┬──────┘ └───────┬───────┘ └──────────┬──────────┘ │ +└─────────┼────────────────┼─────────────────┼─────────────────────┼────────────┘ + │ │ │ │ + ▼ ▼ ▼ ▼ +┌───────────────────────────────────────────────────────────────────────────────┐ +│ INFRASTRUCTURE LAYER (Адаптеры) │ +│ │ +│ ┌─────────────────────────────────┐ ┌─────────────────────────────────┐ │ +│ │ ВХОДЯЩИЕ АДАПТЕРЫ │ │ ИСХОДЯЩИЕ АДАПТЕРЫ │ │ +│ │ ┌─────────────────────────┐ │ │ ┌─────────────────────────┐ │ │ +│ │ │ BookingController │◄───┼────┼──┤ BookingRepository │ │ │ +│ │ │ (REST API) │ │ │ │ (PostgreSQL) │ │ │ +│ │ └─────────────────────────┘ │ │ └─────────────────────────┘ │ │ +│ │ ┌─────────────────────────┐ │ │ ┌─────────────────────────┐ │ │ +│ │ │ AdminController │◄───┼────┼──┤ CourtRepository │ │ │ +│ │ │ (REST API) │ │ │ │ (PostgreSQL) │ │ │ +│ │ └─────────────────────────┘ │ │ └─────────────────────────┘ │ │ +│ │ ┌─────────────────────────┐ │ │ ┌─────────────────────────┐ │ │ +│ │ │ PaymentWebhookController│◄───┼────┼──┤ ScheduleRepository │ │ │ +│ │ │ (REST API) │ │ │ │ (Redis/PostgreSQL) │ │ │ +│ │ └─────────────────────────┘ │ │ └─────────────────────────┘ │ │ +│ │ │ │ ┌─────────────────────────┐ │ │ +│ │ │ │ │ PaymentGatewayClient │───►│ │ +│ │ │ │ │ (HTTP Client) │ │ │ +│ │ │ │ └─────────────────────────┘ │ │ +│ │ │ │ ┌─────────────────────────┐ │ │ +│ │ │ │ │ NotificationClient │───►│ │ +│ │ │ │ │ (HTTP Client) │ │ │ +│ │ │ │ └─────────────────────────┘ │ │ +│ └─────────────────────────────────┘ └─────────────────────────────────┘ │ +│ ▲ ▲ │ +│ │ │ │ +│ └───────────────────┬───────────────┘ │ +│ │ │ +└───────────────────────────────────────────────────┼───────────────────────────┘ + │ + ▼ +┌────────────────────────────────────────────────────────────────────────────────────────┐ +│ APPLICATION LAYER (Порты) │ +│ │ +│ ┌───────────────────────────────────────┐ ┌──────────────────────────────────────┐ │ +│ │ ВХОДЯЩИЕ ПОРТЫ (Интерфейсы) │ │ ИСХОДЯЩИЕ ПОРТЫ (Интерфейсы) │ │ +│ │ │ │ │ │ +│ │ interface IBookingService { │ │ interface IBookingRepository { │ │ +│ │ createBooking(cmd): BookingId │ │ save(booking): void │ │ +│ │ cancelBooking(id): void │ │ findById(id): Booking │ │ +│ │ getBooking(id): BookingDTO │ │ findByUser(userId): List │ │ +│ │ listAvailableSlots(): List │ │ findActiveByCourt(court, │ │ +│ │ } │ │ date) │ │ +│ │ │ │ } │ │ +│ │ interface IAdminService { │ │ │ │ +│ │ createPhoneBooking(cmd): BookingId │ interface ICourtRepository { │ │ +│ │ confirmPayment(id): void │ │ findById(id): Court │ │ +│ │ } │ │ findByType(type): List │ │ +│ │ │ │ findAll(): List │ │ +│ │ interface IPaymentService { │ │ } │ │ +│ │ processPayment(cmd): Result │ │ │ │ +│ │ verifyPayment(id): Status │ │ interface IScheduleRepository{ │ │ +│ │ } │ │ isAvailable(court, slot): bool │ │ +│ │ │ │ lockSlot(court, slot): bool │ │ +│ │ │ │ unlockSlot(court, slot): void │ │ +│ │ │ │ confirmSlot(court, slot): void │ │ +│ │ │ │ } │ │ +│ │ │ │ │ │ +│ │ │ │ interface IPaymentGateway { │ │ +│ │ │ │ charge(amount, currency): Result │ │ +│ │ │ │ refund(paymentId): Result │ │ +│ │ │ │ getStatus(paymentId): Status │ │ +│ │ │ │ } │ │ +│ │ │ │ │ │ +│ │ │ │ interface INotificationService{ │ │ +│ │ │ │ sendBookingConfirmation(to, │ │ +│ │ │ │ booking)│ │ +│ │ │ │ sendReminder(to, booking): void │ │ +│ │ │ │ } │ │ +│ └───────────────────────────────────────┘ └──────────────────────────────────────┘ │ +│ ▲ ▲ │ +│ │ │ │ +│ └─────────────────┬──────────────┘ │ +│ │ │ +└───────────────────────────────────────────────────┼────────────────────────────────────┘ + │ + ▼ +┌──────────────────────────────────────────────────────────────────────────────┐ +│ DOMAIN LAYER (Ядро) │ +│ │ +│ ┌──────────────────────────────────────────────────────────────────────────┐│ +│ │ Entities (Сущности) ││ +│ │ ┌─────────────┐ ┌─────────────┐ ┌──────────────┐ ┌─────────────────┐ ││ +│ │ │ Booking │ │ Court │ │ Slot │ │ Payment │ ││ +│ │ │ (Aggregate)│ │ (Entity) │ │(Value Object)│ │ (Entity) │ ││ +│ │ │ │ │ │ │ │ │ │ ││ +│ │ │ - id: UUID │ │ - id: UUID │ │ - start: Time│ │ - id: UUID │ ││ +│ │ │ - userId │ │ - name │ │ - end: Time │ │ - bookingId │ ││ +│ │ │ - courtId │ │ - type │ │ - date: Date │ │ - amount │ ││ +│ │ │ - slot │ │ - capacity │ │ │ │ - status │ ││ +│ │ │ - status │ │ - price │ │ │ │ - method │ ││ +│ │ │ - payment │ │ - isActive │ │ │ │ │ ││ +│ │ │ │ │ │ │ │ │ │ ││ +│ │ │ confirm() │ │ activate() │ │ overlaps()? │ │ refund() │ ││ +│ │ │ cancel() │ │ deactivate()│ │ │ │ confirm() │ ││ +│ │ │ isExpired() │ │ │ │ │ │ │ ││ +│ │ └─────────────┘ └─────────────┘ └──────────────┘ └─────────────────┘ ││ +│ │ ││ +│ │ ┌─────────────┐ ┌─────────────┐ ┌──────────────┐ ┌─────────────────┐ ││ +│ │ │ User │ │ CourtType │ │ BookingStatus│ │ PaymentStatus │ ││ +│ │ │ (Entity) │ │ (Enum/VO) │ │ (Enum) │ │ (Enum) │ ││ +│ │ │ │ │ │ │ │ │ │ ││ +│ │ │ - id │ │ VOLLEYBALL │ │ PENDING │ │ PENDING │ ││ +│ │ │ - email │ │ BASKETBALL │ │ RESERVED │ │ PROCESSING │ ││ +│ │ │ - phone │ │ BADMINTON │ │ CONFIRMED │ │ SUCCESS │ ││ +│ │ │ - role │ │ TABLE_TENNIS│ │ CANCELLED │ │ FAILED │ ││ +│ │ │ │ │ │ │ EXPIRED │ │ REFUNDED │ ││ +│ │ └─────────────┘ └─────────────┘ └──────────────┘ └─────────────────┘ ││ +│ │ ││ +│ └──────────────────────────────────────────────────────────────────────────┘│ +│ │ +│ ┌──────────────────────────────────────────────────────────────────────────┐│ +│ │ Domain Events (События) ││ +│ │ ││ +│ │ BookingCreatedEvent → После создания бронирования ││ +│ │ BookingConfirmedEvent → После подтверждения оплаты ││ +│ │ BookingCancelledEvent → После отмены ││ +│ │ PaymentReceivedEvent → После получения платежа ││ +│ │ SlotLockedEvent → После блокировки слота ││ +│ │ ││ +│ └──────────────────────────────────────────────────────────────────────────┘│ +│ │ +└──────────────────────────────────────────────────────────────────────────────┘ + +``` + + + +### Часть 2. Структура проекта (скелет) + +**Технология:** Java + +**Структура папок:** + +``` +lab-02/ +├── src/ +│ ├── __init__.py +│ ├── domain/ +│ │ ├── __init__.py +│ │ ├── models/ +│ │ │ ├── __init__.py +│ │ │ ├── booking.py +│ │ │ ├── court.py +│ │ │ ├── user.py +│ │ │ ├── payment.py +│ │ │ └── value_objects/ +│ │ │ ├── __init__.py +│ │ │ ├── slot.py +│ │ │ ├── court_type.py +│ │ │ ├── booking_status.py +│ │ │ ├── payment_status.py +│ │ │ └── money.py +│ │ ├── events/ +│ │ │ ├── __init__.py +│ │ │ ├── domain_event.py +│ │ │ ├── booking_created.py +│ │ │ ├── booking_confirmed.py +│ │ │ ├── booking_cancelled.py +│ │ │ └── payment_received.py +│ │ └── exceptions/ +│ │ ├── __init__.py +│ │ └── domain_exception.py +│ │ +│ ├── application/ +│ │ ├── __init__.py +│ │ ├── ports/ +│ │ │ ├── __init__.py +│ │ │ ├── inn/ +│ │ │ │ ├── __init__.py +│ │ │ │ ├── booking_service.py +│ │ │ │ ├── admin_service.py +│ │ │ │ └── payment_service.py +│ │ │ └── outt/ +│ │ │ ├── __init__.py +│ │ │ ├── booking_repository.py +│ │ │ ├── court_repository.py +│ │ │ ├── schedule_repository.py +│ │ │ ├── user_repository.py +│ │ │ ├── payment_gateway.py +│ │ │ └── notification_service.py +│ │ ├── commands/ +│ │ │ ├── __init__.py +│ │ │ ├── create_booking_command.py +│ │ │ ├── cancel_booking_command.py +│ │ │ └── confirm_payment_command.py +│ │ └── services/ +│ │ └── __init__.py +│ │ +│ └── infrastructure/ +│ ├── __init__.py +│ ├── adapters/ +│ │ ├── __init__.py +│ │ ├── inn/ +│ │ │ ├── __init__.py +│ │ │ ├── booking_controller.py +│ │ │ ├── admin_controller.py +│ │ │ └── payment_webhook_controller.py +│ │ └── outt/ +│ │ ├── __init__.py +│ │ ├── in_memory_booking_repository.py +│ │ ├── in_memory_court_repository.py +│ │ ├── in_memory_schedule_repository.py +│ │ ├── in_memory_user_repository.py +│ │ ├── mock_payment_gateway.py +│ │ └── mock_notification_service.py +│ └── config/ +│ ├── __init__.py +│ └── dependency_injection.py +│ +├── README.md +├── Architecture.md +└── Отчет.md +``` + +**Скриншот структуры в IDE**: + +![Структура](lab2.png) + +--- + +### Часть 3. Domain Layer (Доменный слой) + +#### Доменные сущности + +**Entity 1**: Court (Площадка) + +```python +@dataclass +class Court: + id: str + name: str # "Волейбольная площадка #1" + court_type: CourtType # VOLLEYBALL / BASKETBALL / BADMINTON / TABLE_TENNIS + description: Optional[str] + is_active: bool = True # Доступна для бронирования? + + def deactivate(self) -> None: ... + def activate(self) -> None: ... +``` + +**Entity 12**: Booking (Бронирование) + +```python +@dataclass +class Booking: + id: str + user_id: str # Кто забронировал + court_id: str # Какая площадка + slot: Slot # Когда (дата + время) + status: BookingStatus # PENDING_PAYMENT / RESERVED / CONFIRMED / CANCELLED / EXPIRED + total_amount: Optional[Money] + payment_id: Optional[str] + created_by_admin: bool # True если бронь по телефону + notes: Optional[str] + created_at: datetime + updated_at: datetime + _events: List[DomainEvent] # Доменные события + + def confirm(self, payment_id: Optional[str]) -> None: ... + def cancel(self, reason: Optional[str], cancelled_by: Optional[str]) -> None: ... + def mark_as_reserved(self) -> None: ... + def expire(self) -> None: ... +``` + +**Entity 3**: User (Пользователь) + +```python +@dataclass +class User: + id: str + email: str + phone: str + full_name: str + role: UserRole # CUSTOMER / ADMIN / MANAGER + is_active: bool + + def is_admin(self) -> bool: ... +``` + +**Entity 4**: Payment (Платёж) + +```python +@dataclass +class Payment: + id: str + booking_id: str + amount: Optional[Money] + status: PaymentStatus + external_payment_id: Optional[str] + paid_at: Optional[datetime] + created_at: datetime + + def mark_as_success(self, external_id: str) -> None: ... + def mark_as_failed(self, reason: Optional[str]) -> None: ... + def refund(self) -> None: ... +``` + +**Value Object 1**: Slot (Временной слот) + +```python +@dataclass +class Slot: + court_id: str + date: date + start_time: time # Например, 18:00 + end_time: time # Например, 19:00 (всегда start + 1 час) + + def overlaps(self, other: 'Slot') -> bool: ... +``` + +**Value Object 2**: Money (Денежная сумма) + +```python +@dataclass(frozen=True) +class Money: + amount: float + currency: str = "BYN" # Белорусский рубль + + def add(self, other: 'Money') -> 'Money': ... + def multiply(self, factor: int) -> 'Money': ... +``` + +**Value Object 3**: CourtType (Enum) + +```python +class CourtType(Enum): + VOLLEYBALL = ("volleyball", "Волейбольная площадка", 35) # 1 шт, 35 BYN/час + BASKETBALL = ("basketball", "Баскетбольная площадка", 35) # 1 шт, 35 BYN/час + BADMINTON = ("badminton", "Бадминтонный корт", 25) # 8 шт, 25 BYN/час + TABLE_TENNIS = ("table_tennis", "Стол для настольного тенниса", 15) # 6 шт, 15 BYN/час +``` + +**Доменные исключения:** +- DomainException — базовое исключение +- SlotNotAvailableException — слот уже занят +- PaymentRequiredException — требуется оплата +- BookingNotFoundException — бронирование не найдено +- InvalidBookingStatusException — недопустимый статус + + +#### Бизнес-правила +1.Нельзя создать бронь без указания площадки и времени +2. Слот длится ровно 1 час (валидация в Slot.__post_init__) +3. Нельзя забронировать задним числом (проверка в Application Layer) +4. Для online-бронирования минимум 30 минут до начала слота +5. Отмена возможна не позднее чем за 2 часа до начала (кроме администраторов) +6. Статус бронирования определяет доступные операции (state machine) +--- + +## Часть 4. Application Layer (Прикладной слой) + +#### Входящие порты (Inbound Ports) + +Интерфейсы, которые предоставляет система внешнему миру: + +**IBookingService**: +```python +class IBookingService(ABC): + @abstractmethod + def create_booking(self, command: CreateBookingCommand) -> str: ... + + @abstractmethod + def cancel_booking(self, command: CancelBookingCommand) -> None: ... + + @abstractmethod + def get_booking(self, booking_id: str) -> Optional[Booking]: ... + + @abstractmethod + def list_user_bookings(self, user_id: str) -> List[Booking]: ... + + @abstractmethod + def confirm_payment(self, booking_id: str, payment_id: str) -> None: ... +``` + +**IAdminService**: +```python +class IAdminService(ABC): + @abstractmethod + def create_phone_booking(self, command: CreateBookingCommand, + customer_name: str, customer_phone: str) -> str: ... + + @abstractmethod + def cancel_any_booking(self, booking_id: str, reason: str) -> None: ... + + @abstractmethod + def get_all_bookings(self, date: Optional[str] = None) -> List[Booking]: ... + + @abstractmethod + def block_slot(self, court_id: str, date: str, start_time: str, + reason: str) -> None: ... +``` + +**IPaymentService**: +```python +class IPaymentService(ABC): + @abstractmethod + def process_payment(self, booking_id: str, amount: float, + currency: str) -> str: ... + + @abstractmethod + def verify_payment(self, payment_id: str) -> bool: ... + + @abstractmethod + def refund_payment(self, payment_id: str, + amount: Optional[float] = None) -> bool: ... +``` + +#### Исходящие порты (Inbound Ports) + +Интерфейсы, через которые система взаимодействует с внешним миру: + +**IBookingRepository**: +```python +class IBookingRepository(ABC): + @abstractmethod + def save(self, booking: Booking) -> None: ... + + @abstractmethod + def find_by_id(self, booking_id: str) -> Optional[Booking]: ... + + @abstractmethod + def find_by_user_id(self, user_id: str) -> List[Booking]: ... + + @abstractmethod + def find_by_court_and_date(self, court_id: str, date: date) -> List[Booking]: ... + + @abstractmethod + def find_active_by_slot(self, court_id: str, date: date, + start_time: time) -> Optional[Booking]: ... +``` + +**ICourtRepository**: +```python +class ICourtRepository(ABC): + @abstractmethod + def save(self, court: Court) -> None: ... + + @abstractmethod + def find_by_id(self, court_id: str) -> Optional[Court]: ... + + @abstractmethod + def find_by_type(self, court_type: CourtType) -> List[Court]: ... + + @abstractmethod + def find_all_active(self) -> List[Court]: ... +``` + +**IScheduleRepository**: +```python +class IScheduleRepository(ABC): + @abstractmethod + def is_available(self, court_id: str, date: date, + start_time: time) -> bool: ... + + @abstractmethod + def lock_slot(self, court_id: str, date: date, start_time: time, + booking_id: str, ttl_minutes: int = 10) -> bool: ... + + @abstractmethod + def unlock_slot(self, court_id: str, date: date, + start_time: time) -> None: ... + + @abstractmethod + def confirm_slot(self, court_id: str, date: date, + start_time: time) -> None: ... + + @abstractmethod + def get_available_slots(self, court_id: str, date: date) -> List[Slot]: ... +``` + + +**IPaymentGateway**: +```python +class IPaymentGateway(ABC): + @abstractmethod + def charge(self, amount: float, currency: str, description: str, + idempotency_key: str) -> PaymentResult: ... + + @abstractmethod + def refund(self, payment_id: str, + amount: Optional[float] = None) -> PaymentResult: ... + + @abstractmethod + def get_status(self, payment_id: str) -> PaymentStatus: ... +``` + + +**INotificationService**: +```python +class INotificationService(ABC): + @abstractmethod + def send_booking_confirmation(self, to_email: str, to_phone: Optional[str], + booking_id: str, court_name: str, + slot_date: str, slot_time: str, + qr_code: Optional[str] = None) -> bool: ... + + @abstractmethod + def send_payment_reminder(self, to_email: str, booking_id: str, + hours_left: int) -> bool: ... + + @abstractmethod + def send_cancellation_notice(self, to_email: str, booking_id: str, + reason: Optional[str]) -> bool: ... + + @abstractmethod + def send_sms(self, to_phone: str, message: str) -> bool: ... +``` + +--- + +### Часть 5. Infrastructure Layer (Инфраструктурный слой) + +#### Входящий адаптер: REST API + +**BookingController**: + +```python +class BookingController: + def __init__(self, booking_service: IBookingService): + self._service = booking_service + + def create_booking(self, request: CreateBookingRequest, + user_id: str) -> dict: ... # POST /api/bookings + + def get_booking(self, booking_id: str) -> dict: ... # GET /api/bookings/{id} + + def cancel_booking(self, booking_id: str, user_id: str) -> dict: ... # DELETE /api/bookings/{id} +``` + +**Эндпоинты:** +- `POST /api/bookings` - создание брони +- `GET /api/bookings/{id}` - получение брони +- `DELETE /api/bookings/{id}` - отмена брони + +**Пример запроса/ответа**: + +```json +POST /api/bookings +{ + "court_id": "court-bd-03", + "date": "2025-03-15", + "start_time": "18:00", + "end_time": "19:00", + "payment_method": "online" +} + +Ответ: +{ + "booking_id": "550e8400-e29b-41d4-a716-446655440000" +} +``` + +#### Исходящий адаптер: Repository + +**InMemoryBookingRepository**: +```python +class InMemoryBookingRepository(IBookingRepository): + def __init__(self): + self._storage: Dict[str, Booking] = {} + + def save(self, booking: Booking) -> None: + self._storage[booking.id] = booking + + def find_by_id(self, booking_id: str) -> Optional[Booking]: + return self._storage.get(booking_id) + + def find_by_user_id(self, user_id: str) -> List[Booking]: + return [b for b in self._storage.values() if b.user_id == user_id] + + def find_by_court_and_date(self, court_id: str, date: date) -> List[Booking]: + return [ + b for b in self._storage.values() + if b.court_id == court_id and b.slot.date == date + ] + + def find_active_by_slot(self, court_id: str, date: date, + start_time: time) -> Optional[Booking]: + for booking in self._storage.values(): + if (booking.court_id == court_id and + booking.slot.date == date and + booking.slot.start_time == start_time and + booking.status.value not in ('cancelled', 'expired')): + return booking + return None +``` + +**InMemoryCourtRepository**: +```python +class InMemoryCourtRepository(ICourtRepository): + def __init__(self): + self._storage: Dict[str, Court] = {} + self._init_default_courts() + + def _init_default_courts(self): + courts = [ + Court("court-vb-01", "Волейбольная площадка #1", CourtType.VOLLEYBALL), + Court("court-bb-01", "Баскетбольная площадка #1", CourtType.BASKETBALL), + *[Court(f"court-bd-{i:02d}", f"Бадминтонный корт #{i}", CourtType.BADMINTON) + for i in range(1, 9)], + *[Court(f"court-tt-{i:02d}", f"Стол для настольного тенниса #{i}", CourtType.TABLE_TENNIS) + for i in range(1, 7)], + ] + for court in courts: + self.save(court) +``` + +### Часть 6. Dependency Injection (Конфигурация зависимостей) + +**DIContainer:** + +```python +class DIContainer: + def __init__(self): + # Outgoing Adapters (реализации портов) + self.booking_repository = InMemoryBookingRepository() + self.court_repository = InMemoryCourtRepository() + self.schedule_repository = InMemoryScheduleRepository() + self.user_repository = InMemoryUserRepository() + self.payment_gateway = MockPaymentGateway(failure_rate=0.1) + self.notification_service = MockNotificationService() + + # TODO: Lab #4 - создать Application Services + # self.booking_service = BookingServiceImpl(...) + # self.admin_service = AdminServiceImpl(...) + + # TODO: Lab #4 - создать Incoming Adapters + # self.booking_controller = BookingController(self.booking_service) +``` + +**Как работает DI**: +Создаются бины (компоненты) репозиториев и внешних сервисов. + + +### Часть 7. Тестирование + +#### Юнит-тесты для Booking (Domain) + +```python +def test_create_booking_success(): + slot = Slot( + court_id="court-bd-01", + date=date(2025, 3, 15), + start_time=time(18, 0), + end_time=time(19, 0) + ) + + booking = Booking( + user_id="user-123", + court_id="court-bd-01", + slot=slot, + total_amount=Money(25.0, "BYN") + ) + + assert booking.status == BookingStatus.PENDING_PAYMENT + assert booking.id is not None + +def test_slot_duration_must_be_one_hour(): + with pytest.raises(DomainException): + Slot( + court_id="court-bd-01", + date=date(2025, 3, 15), + start_time=time(18, 0), + end_time=time(20, 0) # 2 часа - ошибка! + ) + +def test_booking_confirm_success(): + booking = create_test_booking() + + booking.confirm(payment_id="PAY-123") + + assert booking.status == BookingStatus.CONFIRMED + assert booking.payment_id == "PAY-123" +``` +**Что тестируется**: +- ✅ Успешное создание брони +- ✅ Валидация длительности слота (ровно 1 час) +- ✅ Подтверждение брони после оплаты +- ✅ Генерация доменных событий + + + + +## 3. Архитектурная диаграмма + +### Диаграмма слоёв +``` +┌───────────────────────────────────────────────────────────────────────────────┐ +│ Infrastructure Layer │ +│ ┌───────────────────────────┐ ┌─────────────────────────────────────────┐ │ +│ │ REST API Controllers │ │ Repositories / External Services │ │ +│ │ (inn/) │ │ (outt/) │ │ +│ │ • BookingController │ │ • InMemoryBookingRepository │ │ +│ │ • AdminController │ │ • InMemoryCourtRepository │ │ +│ │ • PaymentWebhookController│ │ • InMemoryScheduleRepository │ │ +│ └───────────┬───────────────┘ │ • MockPaymentGateway │ │ +│ │ │ • MockNotificationService │ │ +│ │ └─────────────────────────────────────────┘ │ +└──────────────┼────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────────┐ +│ Application Layer │ +│ ┌─────────────────────────┐ ┌─────────────────────────────────────────┐ │ +│ │ Incoming Ports (inn/) │ │ Outgoing Ports (outt/) │ │ +│ │ • IBookingService │ │ • IBookingRepository │ │ +│ │ • IAdminService │ │ • ICourtRepository │ │ +│ │ • IPaymentService │ │ • IScheduleRepository │ │ +│ └───────────┬─────────────┘ │ • IPaymentGateway │ │ +│ │ │ • INotificationService │ │ +│ ┌───────────▼─────────────────────────────────────────────────────────┐ │ +│ │ Commands (DTO): CreateBookingCommand, CancelBookingCommand, ... │ │ +│ └─────────────────────────────────────────────────────────────────────┘ │ +└───────────────────────────┬─────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────────┐ +│ Domain Layer │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ Booking │ │ Court │ │ User │ │ Payment │ │ +│ │ (Aggregate) │ │ (Entity) │ │ (Entity) │ │ (Entity) │ │ +│ │ Root │ │ │ │ │ │ │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ Slot │ │ Money │ │ CourtType │ │ BookingStatus│ │ +│ │ (Value Obj) │ │ (Value Obj) │ │ (Enum) │ │ (Enum) │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ │ +│ ┌─────────────────────────────────────────────────────────────────────┐ │ +│ │ Domain Events: BookingCreated, BookingConfirmed, BookingCancelled │ │ +│ └─────────────────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` +### Описание портов и адаптеров +| Тип | Название | Назначение | +| --- | --- | --- | +| **Входящий порт** | IBookingService | Интерфейс для создания/отмены бронирований клиентами +| **Входящий порт** | IAdminService | Интерфейс для управления бронями администраторами +| **Входящий порт** | IPaymentService | Интерфейс для обработки платежей +| **Исходящий порт** | IBookingRepository | Интерфейс для хранения бронирований +| **Исходящий порт** | ICourtRepository | Интерфейс для доступа к площадкам +| **Исходящий порт** | ICourtRepository | Интерфейс для доступа к площадкам +| **Исходящий порт** | IScheduleRepository | Интерфейс для управления расписанием и блокировкой слотов +| **Исходящий порт** | IPaymentGateway | Интерфейс для интеграции с платёжной системой +| **Исходящий порт** | INotificationService | Интерфейс для отправки уведомлений +| **Входящий адаптер** | BookingController | REST API для клиентов +| **Входящий адаптер** | AdminController | REST API для администраторов +| **Входящий адаптер** | PaymentWebhookController | Webhook от платёжной системы +| **Исходящий адаптер** | InMemoryBookingRepository | Реализация хранилища броней в памяти +| **Исходящий адаптер** | InMemoryCourtRepository | Реализация хранилища площадок в памяти +| **Исходящий адаптер** | InMemoryScheduleRepository | Реализация расписания в памяти +| **Исходящий адаптер** | MockPaymentGateway | Имитация платёжного шлюза +| **Исходящий адаптер** | MockNotificationService | Имитация сервиса уведомлений + +--- + + +## 4. Критерии выполнения + +| Критерий | Выполнено | +|----------|-----------| +| Структура проекта (domain/application/infrastructure) | ✅ | +| Domain Layer (чистая бизнес-логика) | ✅ | +| Порты (входящие и исходящие интерфейсы) | ✅ | +| Адаптеры (минимум 1 входящий + 2 исходящих) | ✅ | +| DI-конфигурация (зависимости инжектятся) | ✅ | +| Юнит-тесты для BookingService с моками | ✅ | +| Документация (диаграмма, описание) | ✅ | + +**Итого**: 7 / 7 + + +## 6. Выводы + +### Что получилось хорошо +Удалось чётко разделить доменный слой (Booking, Court, Slot, Money) и инфраструктуру. Domain-модели не содержат зависимостей от FastAPI или SQLAlchemy. Application-сервисы (интерфейсы портов) легко тестируются с моками, так как работают только через абстракции. Структура проекта получилась чистой и соответствует гексагональной архитектуре. +Особенности предметной области (16 площадок, почасовая оплата, online/phone бронирование) хорошо легли в архитектуру: CourtType с ценами, Slot с валидацией 1 часа, BookingStatus с разными путями подтверждения. + +### С какими трудностями столкнулись +Первоначально было сложно понять, зачем создавать отдельные интерфейсы для портов, если есть только одна реализация. Также возникли сложности с именованием папок (in/out — зарезервированные слова в Python), пришлось переименовать в inn/outt. Настройка импортов в Python (абсолютные vs относительные) потребовала понимания PYTHONPATH и структуры пакетов. + +### Что узнали нового +Узнала, как работает гексагональная архитектура и почему важно отделять бизнес-логику от инфраструктуры. Разобралась в принципе Dependency Inversion: доменный слой не должен зависеть от деталей реализации. Поняла, как порты позволяют легко подменять адаптеры (например, InMemory → PostgreSQL в Lab #5). Освоила базовые принципы DI и поняла, как они помогают тестировать приложение. + +### Как можно улучшить +Добавить реальную БД (PostgreSQL) вместо InMemory-репозиториев (Lab #5). Реализовать полноценную проверку доступности площадок с учётом часов работы манежа (08:00-23:00). Добавить интеграционные тесты с TestContainers (Lab #6). Ввести систему уведомлений (email/SMS) и вынести её в отдельный микросервис (Lab #8). Добавить валидацию входных DTO и централизованную обработку ошибок (Lab #4). Реализовать Saga Pattern для распределённых транзакций (бронирование + оплата).