Skip to content

qFioofa/payment-subscription.springboot

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Умный реестр подписок

Бэкенд для учёта подписок и регулярных платежей. Создаёт обязательства, считает даты списаний, сам меняет статусы и шлёт уведомления о ближайших списаниях.

Стек: Java 21, Spring Boot, PostgreSQL, Flyway, Docker Compose. Схема БД описана в database/README.md.

Как запустить

Нужен только Docker. Все команды — через make.

make run-all

Что делает: проверяет docker, создаёт .env из .env.example если его нет, собирает и поднимает два контейнера — базу и бэкенд. База стартует первой, бэкенд ждёт её готовности.

Миграции применяются сами. Папка database/migration монтируется в контейнер бэкенда, при старте Flyway накатывает V1__init.sql и создаёт таблицы. Руками ничего создавать не надо.

После старта:

Другие команды:

  • make show-logs — логи.
  • make show-status — статус контейнеров.
  • make open-psql — консоль psql в базе.
  • make stop-all — остановить.
  • make clean-all — остановить и удалить данные.

Как запустить тесты

make run-tests

Тесты гоняются в контейнере с Gradle, локальный Java не нужен. Базы в тестах нет — репозиторий замокан через Mockito, Spring не поднимается.

Ответы на вопросы

Почему lazy expiry не трогает рекуррентные. Перед выдачей списка GET /obligations все active с датой в прошлом становятся expired. Но только разовые (recurrence == null). У подписки просроченная дата не значит, что она кончилась — деньги всё равно списываются, пользователь просто не нажал «Оплачено». Поэтому подписка остаётся active, а дату правит оплата.

Как считаются даты при рекуррентности. При оплате дата сдвигается от текущего next_payment_date, а не от дня оплаты — иначе при просрочке копится ошибка. Сдвиг: monthly +1 месяц, quarterly +3 месяца, yearly +1 год. Считает LocalDate, сторонних библиотек нет. Если числа в месяце нет — берётся последний день: 31 января +1 месяц = 28 февраля (или 29 в високосный год), 31 марта +1 месяц = 30 апреля. Разовое при оплате не сдвигается, а закрывается в cancelled.

Какие тесты. Ленивое истечение и исключение для подписок. Оплата для каждого recurrence и для null. Оплата 31-го числа при monthly. Оплата и отмена не-active обязательства. Создание дубля с warning в ответе.

Компромиссы и что бы улучшил.

  • Время берётся через LocalDate.now(). Вынес бы в Clock для детерминированных тестов на граничные даты.
  • Lazy expiry перебирает записи в памяти и сохраняет по одной. На больших объёмах заменил бы на один пакетный UPDATE.
  • SSE держит подписчиков в памяти процесса. Для нескольких инстансов нужен внешний брокер.
  • Курсы валют не считаются, totals группирует по валюте как есть — так требует ТЗ.
  • Тесты только на уровне сервиса. Добавил бы HTTP-тесты через MockMvc на коды 404/422 и формат SSE.

Примеры запросов

Enum передаются именами в верхнем регистре: categorySUBSCRIPTION, WARRANTY, BILL, INSURANCE; recurrenceMONTHLY, QUARTERLY, YEARLY или null. Тела в snake_case.

Создать. Если дата в прошлом — сразу expired, не ошибка.

curl -X POST http://localhost:8080/obligations \
  -H 'Content-Type: application/json' \
  -d '{
    "title": "Яндекс.Плюс",
    "amount": 299.00,
    "currency": "RUB",
    "category": "SUBSCRIPTION",
    "recurrence": "MONTHLY",
    "next_payment_date": "2026-08-01"
  }'

Если активное обязательство с таким title уже есть — запись создаётся, но в ответе есть warning.

{
  "obligation": { "...": "созданная запись" },
  "warning": "Активное обязательство с таким названием уже существует"
}

Список с фильтрами. Оба параметра опциональны, работают вместе, сортировка по next_payment_date.

curl 'http://localhost:8080/obligations?category=SUBSCRIPTION&status=ACTIVE'

Ближайшие списания за N дней (по умолчанию 7). В ответе — список, суммы по валютам и отдельно renewal_alerts (только подписки).

curl 'http://localhost:8080/obligations/upcoming?days=30'
{
  "obligations": [ "..." ],
  "totals": { "RUB": 299.00 },
  "renewal_alerts": [ { "id": "...", "title": "Яндекс.Плюс", "next_payment_date": "2026-08-01", "amount": 299.00, "currency": "RUB" } ]
}

Оплатить. Пишет строку в payments, сдвигает дату или закрывает разовое. Не-active → 422.

curl -X POST http://localhost:8080/obligations/{id}/pay

Отменить. Только из active, иначе 422.

curl -X PATCH http://localhost:8080/obligations/{id}/cancel

Удалить. В любом статусе, вместе с платежами, ответ 204.

curl -X DELETE http://localhost:8080/obligations/{id}

Подписаться на события. После каждого DELETE в поток падает obligation_deleted.

curl -N http://localhost:8080/obligations/events
event: obligation_deleted
data: {"type":"obligation_deleted","id":"..."}

About

Backend on Spring Boot to monitor the payments and subscriptions for AI as interface

Topics

Resources

Stars

Watchers

Forks

Contributors

Languages