Skip to content
mitflegPublic

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Тестовое задание: реферальная программа

Ориентир — 15 минут. Нейросети использовать можно и нужно: Claude, ChatGPT, Cursor, Copilot — что угодно, мы сами так работаем.


1. Как проходит задание

Созвона не будет — не ждите от нас приглашения. Всё делаете сами, в удобное вам время:

прочитали ТЗ → поставили инструменты → включили запись экрана → сделали задание → прислали нам ссылки (как — в конце блока 3).

Записывайте весь процесс целиком: как запускаете проект, что читаете в коде, что спрашиваете у нейросети, как проверяете результат. Можно с голосом — так ещё лучше. Нам важен ход работы, а не только итоговый код.

Перед тем как включать запись, поставьте:

Что Зачем
Программа записи экрана — OBS, Loom или встроенная в систему записать видео выполнения
Docker Desktop (скачать) и запустить его в нём поднимается проект, локальный PHP не нужен
Postman (скачать) или curl дёргать роуты и смотреть ответы
Любой редактор с PHP — VS Code, PhpStorm, Cursor смотреть и править код
Доступ к вашей нейросети пригодится

Вместо Docker подойдёт локальный PHP 8.2+ с Composer, если он у вас уже стоит.

Проверьте заранее, что Docker живой:

docker run --rm hello-world

2. Что в проекте

Мини-сервис реферальной программы MastApp. Мастер приводит другого мастера по своему коду и получает за это вознаграждение.

В базе три таблицы: masters (мастера), payments (их платежи за подписку) и referrals (кто кого привёл). Данные уже засеяны: Маша с кодом MASHA10 и четверо приведённых ею мастеров с разной историей платежей.

Есть модели, сервис ReferralService и обработчик платежей PaymentObserver.


3. Что нужно сделать

Три роута в routes/api.php:

Метод Путь Что делает
POST /api/referrals/attach принимает { "code": "MASHA10" } и закрепляет текущего мастера за владельцем кода. Повторный вызов не создаёт вторую привязку, за себя закрепиться нельзя
GET /api/referrals/my список приведённых мной мастеров: имя, дата привязки, засчитан или нет, сколько по нему начислено
GET /api/referrals/earnings сводка по деньгам: всего начислено, в ожидании, выплачено, сколько рефералов засчитано

Текущий мастер приходит в заголовке X-Master-Id, авторизация заглушена. X-Master-Id: 1 — это Маша.

Формат JSON выбираете сами. Красивая архитектура не нужна — нужно рабочее и честное.

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

Как сдать

  1. Если ещё не сделали — создайте свой репозиторий из этого шаблона: кнопка Use this template → Create a new repository на странице задания на GitHub.
  2. Закоммитьте свою работу и запушьте её в свой репозиторий.
  3. Сделайте репозиторий публичным, чтобы мы могли открыть его без запроса доступа.
  4. Пришлите нам две ссылки:
    • на ваш репозиторий;
    • на видео — YouTube (доступ по ссылке), Google Drive, Яндекс Диск, Loom — куда удобно, главное чтобы открывалось без запроса доступа.

4. Запуск

Команды выполняются из папки проекта.

Шаг 1. Зависимости

docker run --rm -v "$PWD:/app" -w /app composer:2 composer install

В PowerShell вместо $PWD подставьте полный путь: -v "C:\путь\к\проекту:/app". Займёт минуту-две, качается Laravel.

Шаг 2. Конфиг и база

docker run --rm -v "$PWD:/app" -w /app php:8.3-cli sh -c "\
  cp -n .env.example .env; \
  php artisan key:generate --force; \
  touch database/database.sqlite; \
  php artisan migrate --seed --force"

Шаг 3. Сервер

docker run --rm -p 8000:8000 -v "$PWD:/app" -w /app php:8.3-cli \
  php artisan serve --host=0.0.0.0 --port=8000

Если порт 8000 занят — поменяйте на -p 8001:8000 и ходите на 8001.

Проверка, что живой:

curl http://localhost:8000/api/ping
# {"ok":true}

Шаг 4. Свои роуты

Пишете их, дёргаете Postman'ом с заголовком X-Master-Id: 1 и смотрите, сходятся ли числа.

Полезное:

php artisan migrate:fresh --seed   # пересобрать базу с нуля
php artisan route:list             # посмотреть свои роуты
php artisan tinker                 # покопаться в данных руками

Через Docker — та же команда внутри docker run --rm -v "$PWD:/app" -w /app php:8.3-cli ...

5. Реализация и проверка

Маршруты зарегистрированы в routes/api.php, обработчики — в app/Http/Controllers/ReferralController.php.

attach проверяет входной код и вызывает существующий ReferralService::registerReferral(). Первый вызов возвращает 201, повторный — 200 с прежней привязкой. Другой действующий код не меняет пригласившего и статус. Неизвестный код, собственный код и неверный формат дают 422. Без X-Master-Id существующего мастера все три ручки возвращают JSON с кодом 401. Заголовок остаётся заглушкой авторизации, как предусмотрено заданием.

my возвращает data — массив объектов с полями id, referred_master_id, name, attached_at (UTC, ISO 8601), is_rewarded, earned_amount. Засчитанность берётся из referrals.status = rewarded, сумма — из сохранённых referral_earnings.amount. Связанный мастер загружается сразу для всего списка, сумма считается в SQL без отдельного запроса на каждого реферала.

earnings возвращает data с полями total, pending, paid, rewarded_referrals. Начисления отбираются по текущему пригласившему мастеру, суммы разделяются по статусам записей начислений. Счётчик использует статус самой привязки. Обе GET-ручки только читают данные и не пересчитывают историю. Суммы возвращаются целыми числами в единицах базы: валюта и денежная единица в проекте не определены.

Исходные ReferralService, PaymentObserver, правила платежей, конфиг ставки и сидер сохранены. В модель Referral добавлена только связь earnings. Для запуска и выполнения задания понадобились ещё три изменения:

  • Добавлена миграция referral_earnings: модель и обработчик уже используют эту таблицу, но исходных миграций для неё нет. Без неё migrate --seed падал.
  • Отдельная миграция добавляет уникальность referrals.referred_master_id, чтобы база запрещала вторую привязку одного мастера. Laravel firstOrCreate умеет читать созданную конкурентным запросом запись после нарушения уникальности. Старые миграции не редактировались. Если в существующей базе уже есть дубли, новая миграция остановится; автоматически удалять или объединять их нельзя.
  • Удалён несовместимый protected createApplication() из tests/TestCase.php: используется штатная инициализация Laravel 12. До изменения тесты падали с ошибкой видимости метода.

Что требует отдельного решения по бизнес-логике

Эти особенности сохранены, ручки их не исправляют.

Где Что обнаружено и к чему приводит
ReferralService::rewardAmount() Сумма умножается на REFERRAL_PERCENT без деления на 100. При значении 10 платёж 3000 даёт 30000. Название настройки вводит в заблуждение, но именно эта формула зафиксирована в коде.
Payment::isMonetary() и scopeMonetary() Первый метод требует положительную сумму, второй фильтрует только тип card/sbp. Поэтому нулевой платёж попадает в счётчик и блокирует начисление по следующему положительному. Так происходит с Дашей.
PaymentObserver::created() Начисление возможно только при создании подходящего платежа, если привязка уже есть и число платежей card/sbp не больше одного. Привязка после первого платежа не начисляет награду задним числом; продление тоже её не даст. Обновление существующего платежа обработчик не запускает.
PaymentObserver::created() Создание начисления и перевод привязки в rewarded не обёрнуты в общую транзакцию с платежом. Сбой между записями может оставить частично сохранённый результат. Конкурентная обработка платежей также не защищена общей блокировкой.
Master::isPaid() Проверяет наличие любого платежа, включая promo, trial и нулевые суммы. Для определения засчитанного реферала не подходит.
Referral::$fillable Не содержит program, хотя сервис передаёт его в firstOrCreate. Сейчас значение master_invite приходит из значения по умолчанию в БД; при строгом режиме массового заполнения возможна ошибка.

Команды проверки

После установки зависимостей и настройки базы из раздела 4, из корня проекта:

docker run --rm -v "$PWD:/app" -w /app php:8.3-cli \
  php artisan test --fail-on-warning

Тесты используют SQLite :memory: и не очищают рабочую демобазу. Проверено: 34 теста, 176 утверждений, без предупреждений. Покрыты создание, повторы, запрет самопривязки и смены пригласившего, валидация, отсутствие мастера, разделение данных мастеров, статусы выплат, чтение сохранённых сумм, пустая сводка, сценарии платежей и запрет дубля на уровне БД.

Запуск сервера в фоне (если контейнер с этим именем ещё не запущен):

docker run -d --rm --name mastapp-referrals \
  -p 127.0.0.1:8000:8000 -v "$PWD:/app" -w /app php:8.3-cli \
  php artisan serve --host=0.0.0.0 --port=8000

Список и сводка Маши:

curl -sS -H 'Accept: application/json' -H 'X-Master-Id: 1' \
  http://127.0.0.1:8000/api/referrals/my

curl -sS -H 'Accept: application/json' -H 'X-Master-Id: 1' \
  http://127.0.0.1:8000/api/referrals/earnings

На исходных демоданных в списке четыре мастера: Ира — true, 30000; Оля, Катя, Даша — false, 0. Сводка:

{"data":{"total":30000,"pending":30000,"paid":0,"rewarded_referrals":1}}

Привязать Лену к Маше; повторить ту же команду для проверки отсутствия дубля:

curl -i -sS -X POST http://127.0.0.1:8000/api/referrals/attach \
  -H 'Accept: application/json' -H 'Content-Type: application/json' \
  -H 'X-Master-Id: 2' -d '{"code":"MASHA10"}'

Первый вызов на исходной базе — 201, последующие — 200. В списке Маши появится Лена со статусом pending, но сводка останется прежней: Лена платила до привязки. Замена заголовка на X-Master-Id: 1 проверит самопривязку (422); удаление заголовка проверит отсутствие текущего мастера (401).

Маршруты, логи и остановка:

docker exec mastapp-referrals php artisan route:list --path=api
docker logs --tail 50 mastapp-referrals
docker exec mastapp-referrals sh -c 'tail -n 50 storage/logs/laravel.log'
docker stop mastapp-referrals

В контейнере проверены реальные HTTP-ответы 200, 201, 401, 422, повторная привязка и денежная сводка. Созданная проверкой временная привязка удалена; исходные четыре связи сохранены. Новых записей в Laravel-логе после HTTP-проверки не было. В локальном логе осталась ошибка исходного запуска о недостающей таблице.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

Generated from MastApp/backend_test