QuickOffer: учебный API

Задание

Протестируйте API по этим требованиям и по описанию ручек в Swagger. Требования — главный источник: если сервис ведёт себя иначе, чем здесь написано, это дефект сервиса. Если требования и описание в Swagger противоречат друг другу или чего-то не договаривают, это дефект документации — его тоже нужно найти и описать.

Что сдать

Как начать

  1. Возьмите свой токен в кабинете ученика.
  2. Откройте вкладку API, нажмите Authorize и вставьте токен.
  3. В Postman: Authorization → Bearer Token, адрес сервиса — https://swaggerhub.quickoffer.ru. Спецификацию можно импортировать по ссылке https://swaggerhub.quickoffer.ru/openapi.json.

Купоны и активации живут в вашей песочнице: их видят только запросы с вашим токеном. Данные старше 60 дней удаляются.

Общие правила

  1. R-E1

    Все ручки API требуют заголовок Authorization: Bearer <токен>.

    Нет заголовка или токен пустой → 401, reason Authorization required: pass your token as Authorization: Bearer <token>. Токен неверный → 401, reason Invalid token.

  2. R-E2

    Каждый токен работает в своей песочнице: купоны и активации одного токена недоступны другому.

  3. R-E3

    Тело запроса — JSON-объект с заголовком Content-Type: application/json.

    Другой Content-Type → 415. Тело не JSON → 400 Invalid JSON. Тело не объект (массив, строка) → 400. Тело больше 64 КБ → 413.

  4. R-E4

    Даты — ISO 8601 с часовым поясом: 2026-11-20T10:00:00Z или 2026-11-20T13:00:00+03:00. В ответах даты всегда в UTC с Z, с точностью до секунды; дробная часть секунд отбрасывается.

  5. R-E5

    «Сейчас» — момент, когда сервер получил запрос.

  6. R-E6

    Неизвестный путь → 404, reason Not Found. Известный путь с другим методом → 405.

  7. R-E7

    Сервис не отвечает 500 ни на какие входные данные: любой неверный запрос получает понятную ошибку 4xx.

  8. R-E8

    Проверки выполняются в том порядке, в каком перечислены требования ручки, и возвращается первая найденная ошибка.

Создание купона

POST /coupon/create

Ошибки этой ручки: 400 и тело {"success": false, "reason": "…"}.

  1. R-C1

    Обязательные поля: title, amount, validUntil. Поле отсутствует или равно null — ошибка.

    reason title is required, amount is required, validUntil is required.

  2. R-C2

    title — только заглавные латинские буквы и цифры, длина от 5 до 30 символов включительно.

    reason title must match ^[A-Z0-9]{5,30}$.

  3. R-C3

    amount — целое число от 1 до 1000 включительно. Дробное число, строка (даже "100"), true/false не принимаются.

    reason amount must be an integer from 1 to 1000.

  4. R-C4

    validUntil и validSince — даты по R-E4.

    reason validUntil must be a date-time in ISO 8601 with time zone (для validSince — так же).

  5. R-C5

    validSince необязателен. Если его нет, validSince = createdAt. Дата начала в прошлом допустима, в будущем — тоже: тогда купон ещё не начал действовать.

  6. R-C6

    validUntil строго позже validSince. Равные даты — ошибка.

    reason validUntil must be later than validSince.

  7. R-C7

    validUntil не может быть в прошлом или равен «сейчас»: купон, который уже истёк, создать нельзя.

    reason validUntil must be in the future.

  8. R-C8

    title уникален в песочнице. После удаления купона его title снова свободен.

    reason Coupon with this title already exists.

  9. R-C9

    Успех — 200 и все поля CreateCouponResponse. createdAt — время запроса, id уникален. Новый купон ещё не активирован.

Активация купона

POST /coupon/activate

Ошибки этой ручки: 400 и тело {"success": false, "status": "…", "message": "…"}.

  1. R-AC1

    Тело: userId — непустая строка до 64 символов, data — объект, data.title — непустая строка.

    status error, message Invalid value for the userId field / …the data field / …the title field.

  2. R-AC2

    Купон ищется по title точно, с учётом регистра: gift123 и GIFT123 — разные купоны.

    Не найден → status notFound, message Coupon not found.

  3. R-AC3

    Купон одноразовый: если он уже активирован (любым пользователем) и активация не отменена — повторно нельзя.

    status alreadyActivated, message Coupon already activated.

  4. R-AC4

    Раньше validSince активировать нельзя. Ровно в момент validSince — уже можно.

    status tooEarly, message Coupon is not active yet.

  5. R-AC5

    Позже validUntil активировать нельзя. Ровно в момент validUntil — ещё можно.

    status tooLate, message Coupon has expired.

  6. R-AC6

    Один пользователь (userId) может активировать не больше 3 купонов за последние 24 часа. Отменённые активации не считаются.

    4-я активация → status error, message Daily activation limit exceeded.

  7. R-AC7

    Успех — 200 и {"success": true}. Купон получает статус activated.

Отмена активации

POST /coupon/cancel

  1. R-X1

    Тело — как у активации (R-AC1), ошибки формата — status error.

  2. R-X2

    Купон не найден (с учётом регистра) → notFound. Купон не активирован → notActivated. Купон активировал другой userId → notOwner.

  3. R-X3

    Отменить можно в течение 15 минут после активации, включительно. Позже → tooLate.

  4. R-X4

    После отмены купон снова свободен: его может активировать любой пользователь, а отменённая активация не входит в лимит R-AC6.

Карточка купона

GET /coupon/{title}

  1. R-G1

    Поиск по title с учётом регистра. Не найден → 404, reason Coupon not found.

  2. R-G2

    status: activated — есть неотменённая активация; иначе upcoming — «сейчас» раньше validSince, expired — позже validUntil, active — в остальных случаях.

  3. R-G3

    activatedBy и activatedAt — данные неотменённой активации, иначе null.

Список купонов

GET /coupon/list

  1. R-L1

    Сортировка: сначала новые по createdAt, при равенстве — по убыванию id.

  2. R-L2

    status — необязательный фильтр, одно из upcoming, active, expired, activated (как в R-G2). Другое значение → 400.

  3. R-L3

    limit — целое от 1 до 50, по умолчанию 10. offset — целое от 0, по умолчанию 0. Неверное значение → 400.

  4. R-L4

    total — сколько купонов подходит под фильтр без учёта пагинации. offset больше total — 200 и пустой items.

Изменение купона

PATCH /coupon/{title}

  1. R-P0

    Купон ищется по title с учётом регистра, это проверяется первым. Не найден → 404.

  2. R-P1

    Менять можно только amount и validUntil, хотя бы одно из них. Другое поле → 400 Only amount and validUntil can be changed. Пустой объект → 400.

  3. R-P2

    Новые значения проверяются так же, как при создании: R-C3, R-C4, R-C6, R-C7.

  4. R-P3

    Активированный купон менять нельзя → 400 Activated coupon cannot be changed.

  5. R-P4

    Успех — 200 и обновлённая карточка купона.

Удаление купона

DELETE /coupon/{title}

  1. R-D1

    Купон не найден → 404. Активированный купон удалить нельзя → 400 Activated coupon cannot be deleted. Успех — 200 и {"success": true}.

Расчёт заказа с купоном

POST /order/calculate

  1. R-O1

    Обязательные поля: userId (строка как в R-AC1), title (непустая строка), orderTotal (целое от 1 до 1 000 000). Иначе → status error.

  2. R-O2

    Купон не найден → notFound. Купон не активирован → notActivated.

  3. R-O3

    Купон может применить только тот пользователь, который его активировал. Другой userId → notOwner.

  4. R-O4

    Активированный купон действует до validUntil. Позже → tooLate.

  5. R-O5

    Минимальная сумма заказа с купоном — 500. Меньше → minOrderTotal.

  6. R-O6

    total = orderTotal − amount, но не меньше 1. discount = orderTotal − total. Расчёт ничего не сохраняет и купон не тратит: его можно считать сколько угодно раз.

Купоны пользователя

GET /user/{userId}/coupons

  1. R-U1

    Неотменённые активации пользователя, сначала новые. Отменённые не показываются. Пользователь без активаций — 200 и пустой items.