Ориентир — 15 минут. Нейросети использовать можно и нужно: Claude, ChatGPT, Cursor, Copilot — что угодно, мы сами так работаем.
Созвона не будет — не ждите от нас приглашения. Всё делаете сами, в удобное вам время:
прочитали ТЗ → поставили инструменты → включили запись экрана → сделали задание → прислали нам ссылки (как — в конце блока 3).
Записывайте весь процесс целиком: как запускаете проект, что читаете в коде, что спрашиваете у нейросети, как проверяете результат. Можно с голосом — так ещё лучше. Нам важен ход работы, а не только итоговый код.
Перед тем как включать запись, поставьте:
| Что | Зачем |
|---|---|
| Программа записи экрана — OBS, Loom или встроенная в систему | записать видео выполнения |
| Docker Desktop (скачать) и запустить его | в нём поднимается проект, локальный PHP не нужен |
Postman (скачать) или curl |
дёргать роуты и смотреть ответы |
| Любой редактор с PHP — VS Code, PhpStorm, Cursor | смотреть и править код |
| Доступ к вашей нейросети | пригодится |
Вместо Docker подойдёт локальный PHP 8.2+ с Composer, если он у вас уже стоит.
Проверьте заранее, что Docker живой:
docker run --rm hello-worldМини-сервис реферальной программы MastApp. Мастер приводит другого мастера по своему коду и получает за это вознаграждение.
В базе три таблицы: masters (мастера), payments (их платежи за подписку)
и referrals (кто кого привёл). Данные уже засеяны: Маша с кодом MASHA10
и четверо приведённых ею мастеров с разной историей платежей.
Есть модели, сервис ReferralService и обработчик платежей PaymentObserver.
Три роута в 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 выбираете сами. Красивая архитектура не нужна — нужно рабочее и честное.
Два правила мы намеренно не описываем словами: в какой момент реферал считается засчитанным и как считается сумма вознаграждения. Оба уже зафиксированы в коде проекта. Ваши роуты должны вести себя так, как ведёт себя система, а не так, как «обычно бывает в реферальных программах».
- Если ещё не сделали — создайте свой репозиторий из этого шаблона: кнопка Use this template → Create a new repository на странице задания на GitHub.
- Закоммитьте свою работу и запушьте её в свой репозиторий.
- Сделайте репозиторий публичным, чтобы мы могли открыть его без запроса доступа.
- Пришлите нам две ссылки:
- на ваш репозиторий;
- на видео — YouTube (доступ по ссылке), Google Drive, Яндекс Диск, Loom — куда удобно, главное чтобы открывалось без запроса доступа.
Команды выполняются из папки проекта.
docker run --rm -v "$PWD:/app" -w /app composer:2 composer installВ PowerShell вместо $PWD подставьте полный путь: -v "C:\путь\к\проекту:/app".
Займёт минуту-две, качается Laravel.
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"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}Пишете их, дёргаете 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 ...
Маршруты зарегистрированы в 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, чтобы база запрещала вторую привязку одного мастера. LaravelfirstOrCreateумеет читать созданную конкурентным запросом запись после нарушения уникальности. Старые миграции не редактировались. Если в существующей базе уже есть дубли, новая миграция остановится; автоматически удалять или объединять их нельзя. - Удалён несовместимый
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-проверки
не было. В локальном логе осталась ошибка исходного запуска о недостающей таблице.