Files
CU_Points/CLAUDE.md
T
emilandClaude Sonnet 4.6 50b3c4198a feat: initial commit — backend API + student cabinet frontend
- Go backend: auth (JWT), points earn/spend, QR token generation,
  partners, admin grant/stats endpoints with chi router
- Next.js 14 frontend: login, student dashboard, transaction history,
  QR display, partners list
- PostgreSQL migrations (4 tables), Redis cache, Docker Compose
- CORS middleware, role-based route protection, Zustand auth store

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 10:03:27 +03:00

359 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CU Points — CLAUDE.md
## Что это за проект
Поинтовая система лояльности для студентов Центрального Университета (Москва).
Студенты зарабатывают поинты за активность в ЦУ и тратят их у партнёров
(локальные кофейни, столовые, магазины рядом с кампусом).
Аналог Innopolis Club, адаптированный для Москвы. Партнёры — небольшой локальный
бизнес, не федеральные сети.
**Статус:** проект с нуля, чистый репозиторий.
---
## Команда
| Роль | Описание |
|------|----------|
| Тимлид / Продакт / Разраб | Emil — основной пользователь Claude Code |
| Backend-разработчик | разный уровень Go |
| Frontend-разработчик | разный уровень Next.js |
| Аналитик × 2 | требования, исследования, метрики |
| Дизайнер | UI/UX, Figma → компоненты |
| Экономист | бизнес-модель, партнёрские условия |
> **Важно для Claude Code:** в команде разный уровень Go и Next.js.
> Код должен быть хорошо прокомментирован, структура — предсказуемой,
> сложные паттерны — объяснены в комментарии над функцией.
---
## Стек
| Слой | Технология |
|------|-----------|
| Backend | Go 1.22+ |
| Frontend | Next.js 14 (App Router) + TypeScript |
| Стили | Tailwind CSS |
| БД | PostgreSQL 16 |
| Кэш / сессии | Redis 7 |
| Миграции | goose |
| Контейнеры | Docker + Docker Compose |
| Репозиторий | GitHub |
| CI/CD | GitHub Actions |
| Деплой | Yandex Cloud |
| Трекер задач | Kanban (Notion) |
---
## Структура репозитория
```
cu-points/
├── backend/
│ ├── cmd/api/
│ │ └── main.go # точка входа: инициализация, DI, запуск
│ ├── internal/
│ │ ├── config/
│ │ │ └── config.go # конфиг из env-переменных
│ │ ├── auth/
│ │ │ ├── handler.go # HTTP-хендлеры (только парсинг запроса/ответа)
│ │ │ ├── service.go # бизнес-логика аутентификации
│ │ │ ├── repository.go # SQL-запросы
│ │ │ └── jwt.go # генерация и валидация JWT
│ │ ├── points/
│ │ │ ├── handler.go
│ │ │ ├── service.go # earn/spend — самый критичный слой
│ │ │ └── repository.go
│ │ ├── users/
│ │ │ ├── handler.go
│ │ │ ├── service.go
│ │ │ └── repository.go
│ │ ├── partners/
│ │ │ ├── handler.go
│ │ │ ├── service.go
│ │ │ └── repository.go
│ │ ├── admin/
│ │ │ ├── handler.go
│ │ │ └── service.go
│ │ └── middleware/
│ │ ├── auth.go # проверка JWT, прокидывание user_id в контекст
│ │ ├── role.go # проверка роли (student/partner/admin)
│ │ └── logger.go # структурированное логирование запросов
│ └── pkg/
│ ├── db/postgres.go # инициализация pgx pool
│ ├── cache/redis.go # инициализация Redis клиента
│ └── response/json.go # стандартные JSON-ответы (success/error)
├── frontend/
│ ├── app/
│ │ ├── (auth)/login/page.tsx
│ │ ├── (student)/
│ │ │ ├── dashboard/page.tsx # баланс + последние операции
│ │ │ ├── history/page.tsx # полная история транзакций
│ │ │ ├── partners/page.tsx # список партнёров
│ │ │ └── qr/page.tsx # QR-код для оплаты
│ │ ├── (partner)/
│ │ │ └── scan/page.tsx # сканер QR + форма суммы
│ │ └── (admin)/
│ │ ├── dashboard/page.tsx
│ │ └── grant/page.tsx # ручное начисление поинтов
│ ├── components/
│ │ ├── ui/ # атомарные компоненты (Button, Input...)
│ │ ├── BalanceCard.tsx
│ │ ├── TransactionList.tsx
│ │ ├── QRDisplay.tsx
│ │ └── PartnerCard.tsx
│ ├── lib/
│ │ ├── api.ts # fetch-обёртка с baseURL и токеном
│ │ ├── store.ts # Zustand: глобальный стейт
│ │ ├── types.ts # типы для API-ответов
│ │ └── utils.ts # форматирование дат, чисел
│ └── middleware.ts # защита роутов по роли
├── migrations/
│ ├── 00001_init_users.sql
│ ├── 00002_init_partners.sql
│ ├── 00003_init_transactions.sql
│ └── 00004_init_earning_rules.sql
├── .github/workflows/
│ ├── backend-ci.yml
│ └── frontend-ci.yml
├── docker-compose.yml
├── Makefile
├── .env.example
└── CLAUDE.md
```
---
## Соглашения по коду
### Go (Backend)
- **Архитектура:** строго `handler → service → repository`. Бизнес-логика **только в service**.
- **Именование файлов:** `snake_case`. Пакеты: короткие, без подчёркиваний.
- **Ошибки:** всегда явные, никогда не игнорировать `err`. Оборачивать через `fmt.Errorf("context: %w", err)`.
- **HTTP-роутер:** стандартная библиотека `net/http` + `chi`. Не использовать gin/echo/fiber без обсуждения с тимлидом.
- **БД:** только `pgx/v5` или `sqlx` с raw SQL. ORM (gorm и т.п.) — **запрещены**.
- **Логирование:** `log/slog` (стандартная библиотека Go 1.21+), структурированные поля.
- **Конфиг:** только через `internal/config/config.go`. Никаких magic strings по всему коду.
- **Комментарии:** обязательны над каждой экспортируемой функцией и над нетривиальной логикой. На английском.
```go
// SpendPoints debits the given amount from the user's balance
// and records a transaction atomically in a single DB transaction.
// Returns ErrInsufficientBalance if balance < amount.
func (s *Service) SpendPoints(ctx context.Context, req SpendRequest) error {
```
### TypeScript / Next.js (Frontend)
- **Роутер:** App Router (не Pages Router).
- **Стейт:** Zustand — глобальный (user, balance). `useState` — локальный UI-стейт.
- **Запросы к API:** только через `lib/api.ts`. **Не использовать axios**.
- **Компоненты:** функциональные, именованные экспорты: `export function BalanceCard(...)`.
- **Типы:** строгая типизация. **`any` — запрещён**. Типы API-ответов в `lib/types.ts`.
- **Стили:** Tailwind CSS. Кастомный CSS только если Tailwind не покрывает случай.
### Общие правила
- Названия переменных, функций, комментарии — **на английском**.
- Commit messages: `feat:`, `fix:`, `chore:`, `refactor:`, `docs:` + описание на английском.
Пример: `feat: add QR token generation endpoint`
- `TODO` только с ссылкой на задачу: `// TODO(notion:TASK-42): handle expired tokens`
- Все API-эндпоинты с префиксом `/api/v1/`.
---
## Git-стратегия
Trunk-based flow — простой и подходящий для команды нашего размера:
```
main ← всегда стабильная, деплоится автоматически
└── feature/auth-jwt
└── feature/points-spend-qr
└── fix/balance-negative-edge-case
```
- **Никогда не пушить напрямую в `main`.**
- Каждая задача из Notion — отдельная ветка: `feature/<название>` или `fix/<название>`.
- Перед мержем — PR с ревью минимум одного человека.
- CI (lint + tests) должен быть зелёным перед мержем.
---
## Модель данных
```sql
-- Пользователи (студенты, партнёры, администраторы)
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
student_id TEXT UNIQUE, -- только для студентов ЦУ
role TEXT NOT NULL CHECK (role IN ('student', 'partner', 'admin')),
balance INTEGER NOT NULL DEFAULT 0 CHECK (balance >= 0),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Партнёры (кофейни, столовые и т.д.)
CREATE TABLE partners (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id), -- аккаунт кассира
name TEXT NOT NULL,
address TEXT NOT NULL,
max_spend_pct INTEGER NOT NULL DEFAULT 50, -- макс. % покупки оплатить поинтами
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Транзакции — append-only лог, НИКОГДА не удалять записи
CREATE TABLE transactions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id),
partner_id UUID REFERENCES partners(id), -- NULL для earn-транзакций
amount INTEGER NOT NULL, -- > 0 earn, < 0 spend
type TEXT NOT NULL CHECK (type IN ('earn', 'spend', 'admin_grant', 'expire')),
description TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Правила начисления (настраиваются администратором)
CREATE TABLE earning_rules (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
points_amount INTEGER NOT NULL,
trigger_type TEXT NOT NULL CHECK (trigger_type IN ('attendance', 'assignment', 'referral', 'admin')),
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX ON transactions(user_id, created_at DESC);
CREATE INDEX ON transactions(partner_id, created_at DESC);
```
---
## Бизнес-правила (критически важно)
1. **Баланс ≥ 0 всегда** — двойная защита: `CHECK (balance >= 0)` в PostgreSQL + проверка в `points.Service` перед списанием.
2. **Транзакции атомарны** — списание баланса и запись транзакции — одна SQL-транзакция (`BEGIN / COMMIT`).
3. **Поинты не конвертируются в рубли** — студент не может вывести поинты деньгами.
4. **Курс:** 1 поинт = 1 рубль у партнёра. Партнёр получает компенсацию от ЦУ по договору.
5. **Лимит списания:** не более `max_spend_pct`% (по умолчанию 50%) от суммы покупки.
6. **QR-токен одноразовый** — JWT с TTL 5 минут. После использования — записать в Redis (ключ = `used_qr:<jti>`, TTL 5 минут) для защиты от повторного использования.
7. **Публичная оферта** — правила программы доступны без авторизации (требование НК РФ п.68 ст.217).
---
## API эндпоинты (MVP)
```
POST /api/v1/auth/login # email + password → {access_token, refresh_token}
POST /api/v1/auth/refresh # {refresh_token} → {access_token}
GET /api/v1/me # профиль + текущий баланс
GET /api/v1/me/transactions # история (query: limit, offset)
GET /api/v1/me/qr # сгенерировать QR-токен (TTL 5 мин)
GET /api/v1/partners # список активных партнёров (публично)
POST /api/v1/partner/spend # {qr_token, amount} → списать поинты
# доступно только роли 'partner'
POST /api/v1/admin/points/grant # {user_id, amount, description}
GET /api/v1/admin/transactions # все транзакции системы
GET /api/v1/admin/users # список студентов + балансы
GET /api/v1/admin/stats # агрегированная статистика
```
---
## Переменные окружения
Файл `.env.example` в корне репозитория:
```env
# Backend
DATABASE_URL=postgres://user:password@localhost:5432/cupoints?sslmode=disable
REDIS_URL=redis://localhost:6379
JWT_SECRET=your-secret-key-minimum-32-characters
JWT_ACCESS_TTL=15m
JWT_REFRESH_TTL=168h
PORT=8080
ENV=development
# Frontend
NEXT_PUBLIC_API_URL=http://localhost:8080
```
**Никогда не коммитить `.env`** — только `.env.example`.
---
## Makefile
```makefile
make docker-up # поднять postgres + redis
make docker-down # остановить
make migrate-up # применить все миграции
make migrate-down # откатить последнюю миграцию
make run-backend # запустить Go API (hot reload через air)
make run-frontend # запустить Next.js dev
make test # go test ./... + jest
make test-coverage # coverage report
make lint # golangci-lint + eslint + tsc
```
---
## Тестирование
- **Unit-тесты** обязательны для `internal/points/service.go` и `internal/auth/service.go`.
- **Integration-тесты** для критических путей: earn, spend, граничный случай (баланс = 0).
- Тестовая БД: `cupoints_test` в docker-compose.
- **Целевое покрытие:** ≥ 70% для пакетов `points` и `transactions`.
---
## CI (GitHub Actions)
На каждый PR в `main`:
1. `golangci-lint`
2. `go test ./...`
3. `tsc --noEmit` + `eslint`
4. Docker build (проверка что образы собираются)
Мерж только при зелёном CI.
---
## Чего НЕ делать
- Не хранить баланс только в Redis — PostgreSQL source of truth.
- Не писать бизнес-логику в хендлерах.
- Не использовать ORM.
- Не удалять записи из `transactions`.
- Не конвертировать поинты в рубли напрямую студенту.
- Не пушить в `main` напрямую.
- Не коммитить `.env`, ключи, пароли.
- Не использовать `any` в TypeScript.
---
## Приоритет задач (MVP)
1. `docker-compose.yml` + `Makefile` + `.env.example`
2. Миграции (все 4 таблицы)
3. Auth: login, JWT, middleware проверки роли
4. Points: earn/spend с атомарностью и проверкой баланса
5. QR: генерация токена + валидация (одноразовость через Redis)
6. REST API студента (me, transactions, qr)
7. REST API партнёра (spend)
8. REST API администратора (grant, stats)
9. Next.js: кабинет студента (баланс, история, QR)
10. Next.js: интерфейс партнёра (сканер + форма суммы)
11. Next.js: дашборд администратора