Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
b450ffe
refactor domain entities
Moomien Jun 13, 2026
6987a32
mock test for transfer handler
Moomien Jun 13, 2026
3d8b51d
fix: исправил кэш рейтлимитинг с fixed window на sliding window, испр…
Moomien Jun 14, 2026
fae7bba
new accounts service
Moomien Jun 14, 2026
5b85c0c
улучшил check constraint в миграции для защиты бд. переписал transact…
Moomien Jun 16, 2026
924becf
эндпоинт для аккаунтов готов
Moomien Jun 16, 2026
64b003b
написал юнит тесты для хендлеров транзакций
Moomien Jun 17, 2026
649d6da
добавлена jwt аутентификация вокруг трех слоев(usecase, storage, deli…
Moomien Jun 18, 2026
24b18e8
написал эндпоинты аутентификации
Moomien Jun 18, 2026
e297af2
написал миддлвейр аутентификации + исправления
Moomien Jun 18, 2026
f7639a6
поправил main.go
Moomien Jun 18, 2026
e0fd3f2
юнит тесты для хендлеров
Moomien Jun 18, 2026
f2e3491
проверка actions
Moomien Jun 20, 2026
351d18b
ci
Moomien Jun 20, 2026
8a68993
test lint des
Moomien Jun 20, 2026
2a1ab6e
test lint des
Moomien Jun 20, 2026
abe5172
ci fix
Moomien Jun 20, 2026
8fd9987
ci fix
Moomien Jun 20, 2026
5dd3ed8
ci fix
Moomien Jun 20, 2026
d60adac
ci fix
Moomien Jun 20, 2026
a915f7a
last ci fix
Moomien Jun 20, 2026
958cced
реализовал паттерн config. поправил логи в usecase и infrastructure
Moomien Jun 20, 2026
e6dfd95
масс рефактор + e2e тесты
Moomien Jun 20, 2026
8460517
теперь тесты проходят
Moomien Jun 20, 2026
520927c
README.MD
Moomien Jul 30, 2026
29f5d3b
воспроизводимый локальный запуск
Moomien Aug 4, 2026
2ef4289
config fix
Moomien Aug 9, 2026
8b78a55
auth changes
Moomien Aug 9, 2026
b539e7c
Auth refactoring
Moomien Aug 10, 2026
5892f8c
Изменил политику отзыва рефреш-токенов
Moomien Aug 15, 2026
f21ab09
cache: использовал доменные ошибки напрямую
Moomien Aug 15, 2026
bd0a65b
environment fix
Moomien Aug 15, 2026
f9d0f2a
feat добавил идемпотентность переводов через postgresql
Moomien Aug 16, 2026
9d5f988
refactor упростил функцию перевода
Moomien Aug 16, 2026
93dd8ee
refactor: Реструктуризация core + улучшение тестов и DevOps
Moomien Aug 30, 2026
7ac5e52
feat: добавил скрипты тестирования
Moomien Sep 2, 2026
29e53ea
разделил HTTP-слой на app/router и привёл bootstrap к единому виду
Moomien Sep 11, 2026
42a0878
фикс путей + make команда для локального ci
Moomien Sep 11, 2026
d61526f
fix
Moomien Sep 11, 2026
65c18ff
убрал BOM
Moomien Sep 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
.git
.github
.agents
.codex

**/.env
**/.env.*
!**/.env.example

coverage.out
*.log
*.tmp

tasks.md
README.md
e2e
36 changes: 36 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
ENVIRONMENT=development
LOG_LEVEL=debug

HTTP_PORT=8080
HTTP_READ_HEADER_TIMEOUT=5s
HTTP_READ_TIMEOUT=10s
HTTP_WRITE_TIMEOUT=15s
HTTP_IDLE_TIMEOUT=60s
HTTP_SHUTDOWN_TIMEOUT=10s

POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_DB=postgres_bd
POSTGRES_USER=admin
POSTGRES_PASSWORD=change-me
POSTGRES_SSLMODE=disable
POSTGRES_MAX_CONNS=10
POSTGRES_MAX_IDLE_CONNS=5
POSTGRES_CONN_MAX_LIFETIME=30m
POSTGRES_CONN_MAX_IDLE_TIME=5m

REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_USER=
REDIS_PASSWORD=
REDIS_DB=0

ACCESS_TOKEN_SECRET=replace-with-a-long-random-access-secret
REFRESH_TOKEN_SECRET=replace-with-a-different-long-random-refresh-secret
ACCESS_TOKEN_TTL=15m
REFRESH_TOKEN_TTL=720h
ISSUER=processing-local

RATE_LIMIT_PER_MINUTE=60
RATE_LIMIT_PER_HOUR=1000
RATE_LIMIT_PER_DAY=10000
56 changes: 56 additions & 0 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
name: Check
on: [push, pull_request]
jobs:
golangci-lint:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version: "1.25.12"
- name: install deps
run: go mod download
- name: Lint
run: go vet ./...
test:
needs: [golangci-lint]
runs-on: ${{ matrix.os }}
strategy:
matrix:
go-version: ['1.25.12']
os: ['ubuntu-latest', 'windows-latest']
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Setup GO
uses: actions/setup-go@v6
with:
go-version: ${{ matrix.go-version }}
- name: install deps
run: go mod download
- name: Run tests
run: go test ./internal/... -coverprofile=coverage.out
- name: Check coverage threshold
if: runner.os == 'Linux'
run: |
COVERAGE=$(go tool cover -func=coverage.out | grep total | awk '{print $3}' | tr -d '%')
echo "Coverage: $COVERAGE%"
if (( $(echo "$COVERAGE < 25" | bc -l) )); then
echo "Coverage is below 25%"
exit 1
fi

e2e:
needs: [golangci-lint]
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version: "1.25.12"
- name: Run e2e tests
run: go test -count=1 ./e2e/...
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.env
coverage.out
*.test

65 changes: 65 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
ARG GO_VERSION=1.25.12
ARG BUILDER_ALPINE_VERSION=3.24
ARG ALPINE_VERSION=3.22.5

FROM golang:${GO_VERSION}-alpine${BUILDER_ALPINE_VERSION} AS builder

WORKDIR /src

COPY go.mod go.sum ./
RUN go mod download

COPY cmd ./cmd
COPY internal ./internal

RUN CGO_ENABLED=0 GOOS=linux go build \
-trimpath \
-ldflags="-s -w" \
-o /out/processing \
./cmd/server

RUN CGO_ENABLED=0 GOOS=linux go build \
-trimpath \
-ldflags="-s -w" \
-o /out/migrate \
./cmd/migrate

RUN CGO_ENABLED=0 GOOS=linux go build \
-trimpath \
-ldflags="-s -w" \
-o /out/seed \
./cmd/seed

FROM alpine:${ALPINE_VERSION} AS runtime-base

RUN apk add --no-cache ca-certificates tzdata \
&& addgroup -S app \
&& adduser -S -G app app

WORKDIR /app

USER app

FROM runtime-base AS migrator

COPY --from=builder --chown=app:app /out/migrate /app/migrate
COPY --chown=app:app migrations /app/migrations

ENTRYPOINT ["/app/migrate"]

FROM runtime-base AS seeder

COPY --from=builder --chown=app:app /out/seed /app/seed

ENTRYPOINT ["/app/seed"]

FROM runtime-base AS app

COPY --from=builder --chown=app:app /out/processing /app/processing

EXPOSE 8080

HEALTHCHECK --interval=10s --timeout=3s --start-period=5s --retries=3 \
CMD wget -q -O /dev/null http://127.0.0.1:8080/health || exit 1

ENTRYPOINT ["/app/processing"]
112 changes: 112 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# Payment Processing

Это простой backend для внутренних переводов между счетами.

Что тут есть:
- регистрация и логин
- JWT auth
- refresh / logout / logout-all
- получение аккаунта и истории операций
- перевод денег между счетами
- rate limiting через Redis
- PostgreSQL для балансов и идемпотентности
- миграции и seed для development

## Быстрый запуск

Нужны Docker и Docker Compose:

```bash
docker compose up --build
```

После запуска сервис будет доступен по адресу:

```text
http://localhost:8080
```

Проверка готовности:

```bash
make api-health-test
```

## Demo-пользователи

В `development` режиме уже есть два пользователя:

- sender@example.test / DemoPass123!
- receiver@example.test / DemoPass123!

Их счета и стартовые балансы уже созданы через seed.

## Примеры

Готовые smoke-проверки уже есть в make:

```bash
make api-auth-test
make api-accounts-test
```

Если хочешь вручную сделать запрос на перевод, можно так:

```bash
curl -X POST http://localhost:8080/transactions \
-H "Authorization: Bearer <access_token>" \
-H "Idempotency-Key: demo-transfer-1" \
-H "Content-Type: application/json" \
-d '{"receiver_id":"22222222-2222-2222-2222-222222222222","amount":"125.50"}'
```

Важно:
- одинаковый `Idempotency-Key` + тот же payload = тот же результат
- тот же `Idempotency-Key` + другой payload = `409 Conflict`

## Основные роуты

- `POST /auth/register`
- `POST /auth/login`
- `POST /auth/refresh`
- `POST /auth/logout`
- `POST /auth/logout-all`
- `GET /accounts/{id}`
- `GET /accounts/{id}/transactions`
- `POST /transactions`
- `GET /transactions/{id}`
- `GET /health/live`
- `GET /health/ready`

## Что стоит знать

- PostgreSQL — источник правды для балансов и операций
- Redis — для rate limiting
- переводы делаются внутри одной SQL-транзакции
- суммы проверяются до записи, чтобы не было некорректных значений

## Проверки

```bash
go test ./internal/... ./cmd/server/...
```

E2E тесты требуют Docker:

```bash
go test ./e2e/...
```

## Структура

- `cmd/server` — запуск API
- `cmd/migrate` — миграции
- `cmd/seed` — demo seed
- `internal/delivery/http` — HTTP слой
- `internal/usecase` — бизнес-логика
- `internal/domain` — сущности и интерфейсы
- `internal/infrastructure` — PostgreSQL, Redis, config, logger
- `migrations` — SQL миграции
- `e2e` — end-to-end тесты

Это не продакшн-платёжка, а учебный проект, который показывает, как можно сделать нормальный backend с безопасными переводами и базовой архитектурой.
58 changes: 58 additions & 0 deletions cmd/migrate/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package main

import (
"context"
"database/sql"
"fmt"
"log"
"os"
"processing/internal/infrastructure/config"
"time"

_ "github.com/jackc/pgx/v5/stdlib"
"github.com/pressly/goose/v3"
)

const defaultMigrationsDir = "migrations"

func main() {
if err := run(); err != nil {
log.Fatal(err)
}
}

func run() error {
cfg, err := config.LoadPostgres()
if err != nil {
return fmt.Errorf("load config: %w", err)
}

db, err := sql.Open("pgx", cfg.PostgresDSN())
if err != nil {
return fmt.Errorf("open postgres: %w", err)
}
defer db.Close()

pingCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := db.PingContext(pingCtx); err != nil {
return fmt.Errorf("ping postgres: %w", err)
}

if err := goose.SetDialect("postgres"); err != nil {
return fmt.Errorf("set goose dialect: %w", err)
}
if err := goose.UpContext(context.Background(), db, migrationsDir()); err != nil {
return fmt.Errorf("apply migrations: %w", err)
}

log.Println("database migrations are up to date")
return nil
}

func migrationsDir() string {
if dir := os.Getenv("MIGRATIONS_DIR"); dir != "" {
return dir
}
return defaultMigrationsDir
}
Loading