Compare commits
4 Commits
c2757d24fc
...
f82a7c7606
| Author | SHA1 | Date | |
|---|---|---|---|
| f82a7c7606 | |||
| b58fd11966 | |||
| ed33357af2 | |||
| 5f85006e1d |
+60
-13
@@ -4,6 +4,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- stage
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
@@ -13,17 +14,51 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Resolve environment
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Тег образа зависит только от коммита (одинаков для stage и prod)
|
||||
echo "IMAGE_TAG=$(git rev-parse --short HEAD)" >> "$GITHUB_ENV"
|
||||
if [[ "${{ gitea.ref_name }}" == "stage" ]]; then
|
||||
{
|
||||
echo "ENV_NAME=stage"
|
||||
echo "CONTAINER_SUFFIX=-stage"
|
||||
echo "APP_PORT=8004"
|
||||
echo "POSTGRES_PORT=5433"
|
||||
echo "REDIS_PORT=6380"
|
||||
echo "COMPOSE_PROJECT=g2s-aggregator-stage"
|
||||
echo "DEPLOY_DIR=/home/deploy/g2s-aggregator-stage"
|
||||
} >> "$GITHUB_ENV"
|
||||
else
|
||||
{
|
||||
echo "ENV_NAME=prod"
|
||||
echo "CONTAINER_SUFFIX="
|
||||
echo "APP_PORT=8003"
|
||||
echo "POSTGRES_PORT=5432"
|
||||
echo "REDIS_PORT=6379"
|
||||
echo "COMPOSE_PROJECT=g2s-aggregator"
|
||||
echo "DEPLOY_DIR=/home/deploy/g2s-aggregator"
|
||||
} >> "$GITHUB_ENV"
|
||||
fi
|
||||
|
||||
- name: Build and push image
|
||||
run: |
|
||||
docker login gitea.p4r4dls.ru \
|
||||
-u ${{ secrets.REGISTRY_USER }} \
|
||||
-p ${{ secrets.REGISTRY_TOKEN }}
|
||||
docker buildx create --use --name multibuilder
|
||||
docker buildx build \
|
||||
--platform linux/amd64 \
|
||||
--push \
|
||||
-t gitea.p4r4dls.ru/yusupal1ev/g2s-aggregator:0.1.0 \
|
||||
.
|
||||
IMG=gitea.p4r4dls.ru/yusupal1ev/g2s-aggregator:${IMAGE_TAG}
|
||||
if docker manifest inspect "$IMG" >/dev/null 2>&1; then
|
||||
echo "Образ $IMG уже есть в реестре — сборка пропущена"
|
||||
else
|
||||
docker buildx rm multibuilder 2>/dev/null || true
|
||||
docker buildx create --use --name multibuilder --driver-opt network=host
|
||||
docker buildx build \
|
||||
--platform linux/amd64 \
|
||||
--load \
|
||||
-t "$IMG" \
|
||||
.
|
||||
docker push "$IMG"
|
||||
fi
|
||||
|
||||
- name: Setup SSH
|
||||
run: |
|
||||
@@ -46,12 +81,24 @@ jobs:
|
||||
value="${entry#*=}"
|
||||
export "$key=$value"
|
||||
done < <(echo "$ALL_SECRETS" | jq -j 'to_entries[] | "\(.key)=\(.value)\u0000"')
|
||||
|
||||
|
||||
# Для stage подменяем базовые имена секретов на STAGE_-версии:
|
||||
# STAGE_FOO -> FOO
|
||||
if [[ "$ENV_NAME" == "stage" ]]; then
|
||||
while IFS= read -r -d '' entry; do
|
||||
key="${entry%%=*}"
|
||||
value="${entry#*=}"
|
||||
if [[ "$key" == STAGE_* ]]; then
|
||||
export "${key#STAGE_}=$value"
|
||||
fi
|
||||
done < <(echo "$ALL_SECRETS" | jq -j 'to_entries[] | "\(.key)=\(.value)\u0000"')
|
||||
fi
|
||||
|
||||
render() {
|
||||
local src=$1 dst=$2
|
||||
vars=$(grep -oP '\$\{\K[^}]+' "$src")
|
||||
for var in $vars; do
|
||||
if [[ -z "${!var:-}" ]]; then
|
||||
if [[ -z "${!var+x}" ]]; then
|
||||
echo "Error: $var не задан в Gitea Secrets" >&2
|
||||
exit 1
|
||||
fi
|
||||
@@ -63,17 +110,17 @@ jobs:
|
||||
render docker-compose.template.yml docker-compose.rendered.yml
|
||||
|
||||
scp -i ~/.ssh/deploy_key config.rendered.yaml \
|
||||
deploy@194.58.121.203:/home/deploy/g2s-aggregator/config.yaml
|
||||
deploy@194.58.121.203:$DEPLOY_DIR/config.yaml
|
||||
scp -i ~/.ssh/deploy_key docker-compose.rendered.yml \
|
||||
deploy@194.58.121.203:/home/deploy/g2s-aggregator/docker-compose.yml
|
||||
deploy@194.58.121.203:$DEPLOY_DIR/docker-compose.yml
|
||||
|
||||
- name: Deploy
|
||||
run: |
|
||||
ssh -i ~/.ssh/deploy_key deploy@194.58.121.203 "
|
||||
cd /home/deploy/g2s-aggregator &&
|
||||
cd $DEPLOY_DIR &&
|
||||
docker login gitea.p4r4dls.ru \
|
||||
-u ${{ secrets.REGISTRY_USER }} \
|
||||
-p ${{ secrets.REGISTRY_TOKEN }} &&
|
||||
docker compose pull &&
|
||||
docker compose up -d
|
||||
docker compose -p $COMPOSE_PROJECT pull &&
|
||||
docker compose -p $COMPOSE_PROJECT up -d
|
||||
"
|
||||
|
||||
+10
-7
@@ -1,17 +1,20 @@
|
||||
FROM python:3.14-slim
|
||||
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
ENV PYTHONUNBUFFERED=1 \
|
||||
UV_COMPILE_BYTECODE=1 \
|
||||
UV_LINK_MODE=copy \
|
||||
UV_FROZEN=1 \
|
||||
PATH="/app/.venv/bin:$PATH"
|
||||
|
||||
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN pip install --no-cache-dir poetry
|
||||
|
||||
COPY pyproject.toml poetry.lock ./
|
||||
RUN poetry config virtualenvs.create false \
|
||||
&& poetry install --no-interaction --no-root
|
||||
COPY pyproject.toml uv.lock ./
|
||||
RUN uv sync --frozen --no-install-project --no-dev
|
||||
|
||||
COPY app ./app
|
||||
COPY alembic.ini ./alembic.ini
|
||||
COPY alembic ./alembic
|
||||
|
||||
CMD ["poetry", "run", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
CMD ["uv", "run", "--no-sync", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
You are Implementer.
|
||||
|
||||
You implement EXACTLY ONE next uncompleted task from spec/tasks/.
|
||||
|
||||
Before starting, you MUST read:
|
||||
- spec/overview.md
|
||||
- the assigned task file in spec/tasks/
|
||||
|
||||
You MUST NOT modify any files in spec/.
|
||||
|
||||
---
|
||||
|
||||
## Your responsibilities
|
||||
|
||||
- Implement the task exactly as specified
|
||||
- Place business rules in Business Logic
|
||||
- Place orchestration in Service
|
||||
- Respect all constraints from the task
|
||||
- Add or update required tests
|
||||
- Keep changes strictly within task scope
|
||||
|
||||
---
|
||||
|
||||
## Mandatory verification steps (BEFORE finishing)
|
||||
|
||||
You MUST:
|
||||
1. Verify all Definition of Done items from the task
|
||||
2. Ensure Business Logic is fully unit-tested WITHOUT mocks
|
||||
3. Ensure Service tests (if any) may use mocks/stubs
|
||||
4. Run all tests specified in the task
|
||||
5. Run all linters / type checks specified in the task
|
||||
6. Verify no unrelated files were modified
|
||||
7. Stage all intended changes:
|
||||
- run: git add -A
|
||||
- DO NOT commit
|
||||
|
||||
If ANY item fails, you MUST report it explicitly.
|
||||
|
||||
---
|
||||
|
||||
## Output format (MANDATORY)
|
||||
|
||||
Output EXACTLY the following sections.
|
||||
|
||||
## Task
|
||||
- Task ID and title
|
||||
|
||||
## Changes made
|
||||
- List of changed files
|
||||
- Short description per file
|
||||
- Explicit layer for each file (Controller / Service / Business Logic / Repository / Adapter / Tests)
|
||||
|
||||
## Definition of Done verification
|
||||
- Checklist copied from the task
|
||||
- Each item marked as PASSED or FAILED
|
||||
|
||||
## Tests
|
||||
- Tests added or updated
|
||||
- Test type:
|
||||
- unit (no mocks)
|
||||
- integration
|
||||
- other
|
||||
- Results
|
||||
|
||||
## Commands executed
|
||||
- Exact commands run
|
||||
- Result (success/failure)
|
||||
|
||||
## Git staging
|
||||
- Confirm: git add -A executed
|
||||
- Confirm: no commits created
|
||||
|
||||
## Notes
|
||||
- Edge cases, trade-offs, or limitations
|
||||
- If none, write "None"
|
||||
|
||||
---
|
||||
|
||||
## Hard rules
|
||||
|
||||
- No scope expansion
|
||||
- No speculative refactoring
|
||||
- No changes outside task scope
|
||||
- No spec changes
|
||||
- No business logic in Service or Adapter
|
||||
@@ -1,99 +0,0 @@
|
||||
You are Planner.
|
||||
|
||||
You analyze a programming task or specification and produce planning artifacts.
|
||||
You do NOT write production code.
|
||||
|
||||
You MUST always read:
|
||||
- spec/overview.md
|
||||
- spec/index.md
|
||||
- existing files in spec/tasks/
|
||||
|
||||
You are the ONLY role allowed to create or modify files in spec/tasks/.
|
||||
Write tasks in russian except headings, frontmatter and terms
|
||||
|
||||
Follow AGENTS.md strictly.
|
||||
|
||||
---
|
||||
|
||||
## Your responsibilities
|
||||
|
||||
Depending on the input, you may:
|
||||
- create one or more new task files in spec/tasks/
|
||||
- update existing task files in spec/tasks/
|
||||
|
||||
For every change in spec/tasks/, you MUST:
|
||||
- use the minimal task format defined below
|
||||
- update spec/index.md via the generator workflow
|
||||
|
||||
You MUST NOT:
|
||||
- modify any source code
|
||||
- change files outside spec/
|
||||
- introduce architectural decisions not requested
|
||||
|
||||
---
|
||||
|
||||
## Minimal task format (MANDATORY)
|
||||
|
||||
Every task file in spec/tasks/ MUST follow this structure.
|
||||
|
||||
### Front matter (required)
|
||||
|
||||
---
|
||||
id: <numeric, zero-padded, unique>
|
||||
title: <short, precise, action-oriented>
|
||||
status: TODO
|
||||
created: <YYYY-MM-DD>
|
||||
---
|
||||
|
||||
### Body (required sections, minimal)
|
||||
|
||||
## Context
|
||||
Why this task exists. Short and factual.
|
||||
|
||||
## Goal
|
||||
What must be implemented or changed.
|
||||
|
||||
## Constraints
|
||||
Hard limits:
|
||||
- architectural
|
||||
- scope
|
||||
- forbidden changes
|
||||
|
||||
## Acceptance criteria
|
||||
- Bullet list
|
||||
- Objective and testable
|
||||
|
||||
## Definition of Done
|
||||
Checklist. Task is DONE only if ALL items are satisfied.
|
||||
|
||||
## Tests
|
||||
What tests must exist or be updated.
|
||||
|
||||
## Commands
|
||||
Exact commands to verify completion
|
||||
(e.g. pytest, ruff, mypy, docker compose).
|
||||
|
||||
---
|
||||
|
||||
## Planner output rules
|
||||
|
||||
When creating or updating tasks, output MUST include:
|
||||
|
||||
### Tasks created
|
||||
- List of new spec/tasks/*.md files with full content
|
||||
|
||||
### Tasks updated
|
||||
- List of modified spec/tasks/*.md files with changes
|
||||
|
||||
### Index update
|
||||
- Explicit instruction to run:
|
||||
python3 spec/gen_spec_index.py
|
||||
|
||||
If no tasks are created or updated, explicitly state:
|
||||
- "No task changes required"
|
||||
|
||||
Rules:
|
||||
- No code blocks outside task files
|
||||
- No speculative tasks
|
||||
- No refactoring-only tasks
|
||||
- No vague goals
|
||||
@@ -1,93 +0,0 @@
|
||||
You are Reviewer.
|
||||
|
||||
You review code changes produced by Implementer.
|
||||
|
||||
Before reviewing, you MUST read:
|
||||
- spec/overview.md
|
||||
- the assigned task file in spec/tasks/
|
||||
|
||||
You MUST review ONLY staged changes:
|
||||
- git diff --staged
|
||||
|
||||
You MUST NOT modify source code.
|
||||
|
||||
---
|
||||
|
||||
## Review scope (MANDATORY)
|
||||
|
||||
You must verify:
|
||||
|
||||
1. Task compliance
|
||||
- Implementation matches task requirements
|
||||
- No scope expansion
|
||||
|
||||
2. Architecture compliance
|
||||
- Correct layer separation
|
||||
- Business Logic purity
|
||||
- Proper Service orchestration
|
||||
- No business decisions in Repository or Adapter
|
||||
|
||||
3. Testing strategy
|
||||
- Business Logic has unit tests WITHOUT mocks
|
||||
- Tests cover task Definition of Done
|
||||
- No missing critical tests
|
||||
|
||||
4. Project compliance
|
||||
- Matches constraints and invariants from spec/overview.md
|
||||
|
||||
---
|
||||
|
||||
## Output format (MANDATORY)
|
||||
|
||||
Comments must be in russian language.
|
||||
Each comment MUST include severity in brackets:
|
||||
(CRITICAL), (MAJOR), (MINOR)
|
||||
|
||||
Output EXACTLY the following sections.
|
||||
|
||||
## Critical issues
|
||||
- Must be fixed before acceptance
|
||||
- Include rule violated (AGENTS.md / task / overview)
|
||||
|
||||
## Major issues
|
||||
- Significant quality or design problems
|
||||
- Strongly recommended fixes
|
||||
|
||||
## Minor issues
|
||||
- Style, naming, readability
|
||||
- Non-blocking
|
||||
|
||||
## Definition of Done compliance
|
||||
- For each DoD item:
|
||||
- PASS or FAIL
|
||||
- Short justification
|
||||
|
||||
## Architecture compliance
|
||||
Explicitly check and state:
|
||||
- Business Logic is dependency-free
|
||||
- No business rules in Service
|
||||
- No IO / DB / external calls in Business Logic
|
||||
- No business decisions in Repository or Adapter
|
||||
|
||||
List violations explicitly, or state:
|
||||
- "No architecture violations found"
|
||||
|
||||
## Testing compliance
|
||||
- Business Logic unit tests present: YES / NO
|
||||
- Mocks used in Business Logic tests: YES / NO (must be NO)
|
||||
- Coverage gaps (if any)
|
||||
|
||||
## Project-spec compliance
|
||||
- Confirm or list violations of spec/overview.md
|
||||
|
||||
## Review input
|
||||
- Confirm you reviewed: git diff --staged
|
||||
|
||||
---
|
||||
|
||||
## Hard rules
|
||||
|
||||
- No code changes
|
||||
- No new requirements
|
||||
- No architectural redesign
|
||||
- No speculative improvements
|
||||
@@ -35,8 +35,8 @@ adapter:
|
||||
|
||||
tbank_payment:
|
||||
init_url: "https://securepay.tinkoff.ru/v2/Init"
|
||||
notification_url: "https://aggregator.get2send.com/api/v1/delivery/tbank/notifications"
|
||||
success_url: "https://aggregator.get2send.com/checkout/success"
|
||||
notification_url: "${TBANK_NOTIFICATION_URL}"
|
||||
success_url: "${TBANK_SUCCESS_URL}"
|
||||
auth:
|
||||
terminal_key: "${TBANK_TERMINAL_KEY}"
|
||||
password: "${TBANK_PASSWORD}"
|
||||
|
||||
+23
-21
@@ -1,9 +1,9 @@
|
||||
services:
|
||||
app:
|
||||
image: gitea.p4r4dls.ru/yusupal1ev/g2s-aggregator:0.1.0
|
||||
container_name: g2s-aggregator
|
||||
image: gitea.p4r4dls.ru/yusupal1ev/g2s-aggregator:${IMAGE_TAG}
|
||||
container_name: g2s-aggregator${CONTAINER_SUFFIX}
|
||||
ports:
|
||||
- "8003:8000"
|
||||
- "${APP_PORT}:8000"
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_started
|
||||
@@ -17,23 +17,23 @@ services:
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
tag: "g2s-aggregator"
|
||||
tag: "g2s-aggregator${CONTAINER_SUFFIX}"
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: redis
|
||||
container_name: redis${CONTAINER_SUFFIX}
|
||||
command: ["redis-server", "--save", "", "--appendonly", "no"]
|
||||
ports:
|
||||
- "6379:6379"
|
||||
- "${REDIS_PORT}:6379"
|
||||
restart: unless-stopped
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
container_name: postgres
|
||||
container_name: postgres${CONTAINER_SUFFIX}
|
||||
environment:
|
||||
POSTGRES_DB: g2s_aggregator
|
||||
POSTGRES_USER: ${POSTGRES_USER}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
ports:
|
||||
- "5432:5432"
|
||||
- "${POSTGRES_PORT}:5432"
|
||||
volumes:
|
||||
- postgres:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
@@ -42,22 +42,22 @@ services:
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
migrations:
|
||||
image: gitea.p4r4dls.ru/yusupal1ev/g2s-aggregator:0.1.0
|
||||
container_name: g2s-aggregator-migrations
|
||||
image: gitea.p4r4dls.ru/yusupal1ev/g2s-aggregator:${IMAGE_TAG}
|
||||
container_name: g2s-aggregator-migrations${CONTAINER_SUFFIX}
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- ./config.yaml:/config.yaml
|
||||
command: ["poetry", "run", "alembic", "upgrade", "head"]
|
||||
command: ["uv", "run", "--no-sync", "alembic", "upgrade", "head"]
|
||||
restart: "no"
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
tag: "g2s-aggregator-migrations"
|
||||
tag: "g2s-aggregator-migrations${CONTAINER_SUFFIX}"
|
||||
waybill-poller:
|
||||
image: gitea.p4r4dls.ru/yusupal1ev/g2s-aggregator:0.1.0
|
||||
container_name: g2s-aggregator-waybill-poller
|
||||
image: gitea.p4r4dls.ru/yusupal1ev/g2s-aggregator:${IMAGE_TAG}
|
||||
container_name: g2s-aggregator-waybill-poller${CONTAINER_SUFFIX}
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
@@ -65,15 +65,15 @@ services:
|
||||
condition: service_completed_successfully
|
||||
volumes:
|
||||
- ./config.yaml:/config.yaml
|
||||
command: ["poetry", "run", "python", "-m", "app.workers.waybill_poller"]
|
||||
command: ["uv", "run", "--no-sync", "python", "-m", "app.workers.waybill_poller"]
|
||||
restart: unless-stopped
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
tag: "g2s-aggregator-waybill-poller"
|
||||
tag: "g2s-aggregator-waybill-poller${CONTAINER_SUFFIX}"
|
||||
waybill-email-sender:
|
||||
image: gitea.p4r4dls.ru/yusupal1ev/g2s-aggregator:0.1.0
|
||||
container_name: g2s-aggregator-waybill-email-sender
|
||||
image: gitea.p4r4dls.ru/yusupal1ev/g2s-aggregator:${IMAGE_TAG}
|
||||
container_name: g2s-aggregator-waybill-email-sender${CONTAINER_SUFFIX}
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
@@ -82,16 +82,18 @@ services:
|
||||
volumes:
|
||||
- ./config.yaml:/config.yaml
|
||||
command:
|
||||
["poetry", "run", "python", "-m", "app.workers.waybill_email_sender"]
|
||||
["uv", "run", "--no-sync", "python", "-m", "app.workers.waybill_email_sender"]
|
||||
restart: unless-stopped
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
tag: "g2s-aggregator-waybill-email-sender"
|
||||
tag: "g2s-aggregator-waybill-email-sender${CONTAINER_SUFFIX}"
|
||||
otel-collector:
|
||||
image: otel/opentelemetry-collector-contrib:latest
|
||||
container_name: otel-collector
|
||||
container_name: otel-collector${CONTAINER_SUFFIX}
|
||||
user: "0"
|
||||
environment:
|
||||
ENV_NAME: ${ENV_NAME}
|
||||
volumes:
|
||||
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
|
||||
- /var/lib/docker/containers:/var/lib/docker/containers:ro
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# spec/overview.md
|
||||
# overview.md
|
||||
|
||||
Контекст, специфичный для проекта агрегатора служб доставки.
|
||||
Глобальные правила определены в AGENTS.md.
|
||||
Generated
-2128
File diff suppressed because it is too large
Load Diff
+27
-31
@@ -1,40 +1,36 @@
|
||||
[tool.poetry]
|
||||
[project]
|
||||
name = "g2s-aggregator"
|
||||
version = "0.1.0"
|
||||
description = ""
|
||||
authors = ["Your Name <you@example.com>"]
|
||||
readme = "README.md"
|
||||
package-mode = false
|
||||
requires-python = ">=3.14"
|
||||
dependencies = [
|
||||
"fastapi>=0.135.1,<0.136.0",
|
||||
"uvicorn[standard]>=0.41.0,<0.42.0",
|
||||
"httpx>=0.28.1,<0.29.0",
|
||||
"pydantic>=2.12.5,<3.0.0",
|
||||
"pydantic-settings>=2.13.1,<3.0.0",
|
||||
"aioredis>=2.0.1,<3.0.0",
|
||||
"structlog>=25.5.0,<26.0.0",
|
||||
"opentelemetry-api>=1.40.0,<2.0.0",
|
||||
"opentelemetry-sdk>=1.40.0,<2.0.0",
|
||||
"opentelemetry-exporter-otlp>=1.40.0,<2.0.0",
|
||||
"opentelemetry-instrumentation-fastapi>=0.61b0",
|
||||
"opentelemetry-instrumentation-httpx>=0.61b0",
|
||||
"opentelemetry-instrumentation-redis>=0.61b0",
|
||||
"pytest>=9.0.2,<10.0.0",
|
||||
"redis>=7.3.0,<8.0.0",
|
||||
"sqlalchemy>=2.0.49,<3.0.0",
|
||||
"asyncpg>=0.31.0,<0.32.0",
|
||||
"alembic>=1.18.4,<2.0.0",
|
||||
"aiosqlite>=0.22.1,<0.23.0",
|
||||
"aiosmtplib>=4.0.2,<5.0.0",
|
||||
"email-validator>=2.2.0,<3.0.0",
|
||||
]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.14"
|
||||
fastapi = "^0.135.1"
|
||||
uvicorn = {extras = ["standard"], version = "^0.41.0"}
|
||||
httpx = "^0.28.1"
|
||||
pydantic = "^2.12.5"
|
||||
pydantic-settings = "^2.13.1"
|
||||
aioredis = "^2.0.1"
|
||||
structlog = "^25.5.0"
|
||||
opentelemetry-api = "^1.40.0"
|
||||
opentelemetry-sdk = "^1.40.0"
|
||||
opentelemetry-exporter-otlp = "^1.40.0"
|
||||
opentelemetry-instrumentation-fastapi = "^0.61b0"
|
||||
opentelemetry-instrumentation-httpx = "^0.61b0"
|
||||
opentelemetry-instrumentation-redis = "^0.61b0"
|
||||
pytest = "^9.0.2"
|
||||
redis = "^7.3.0"
|
||||
sqlalchemy = "^2.0.49"
|
||||
asyncpg = "^0.31.0"
|
||||
alembic = "^1.18.4"
|
||||
aiosqlite = "^0.22.1"
|
||||
aiosmtplib = "^4.0.2"
|
||||
email-validator = "^2.2.0"
|
||||
[tool.uv]
|
||||
package = false
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
pythonpath = ["."]
|
||||
addopts = ["--import-mode=importlib"]
|
||||
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
@@ -1,229 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Generate spec/index.md from spec/tasks/*.md
|
||||
|
||||
Assumptions:
|
||||
- Each task file starts with YAML-like front matter:
|
||||
|
||||
---
|
||||
id: 001
|
||||
title: Add endpoint /reports/addons
|
||||
status: TODO # TODO | DONE
|
||||
created: 2026-02-01
|
||||
---
|
||||
|
||||
Notes:
|
||||
- status lives inside each task file (as requested).
|
||||
- index.md is fully overwritten and marked as generated.
|
||||
- No external deps (no PyYAML). Supports simple "key: value" scalars.
|
||||
|
||||
Usage:
|
||||
python3 spec/gen_spec_index.py
|
||||
python3 spec/gen_spec_index.py --check # verify index is up-to-date (CI)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import datetime as dt
|
||||
import re
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SPEC_DIR = ROOT / "spec"
|
||||
TASKS_DIR = SPEC_DIR / "tasks"
|
||||
INDEX_PATH = SPEC_DIR / "index.md"
|
||||
|
||||
ALLOWED_STATUS = {"TODO", "DONE"}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TaskMeta:
|
||||
file: Path
|
||||
id: str
|
||||
title: str
|
||||
status: str
|
||||
created: str # YYYY-MM-DD
|
||||
|
||||
|
||||
FRONT_MATTER_RE = re.compile(r"^---\s*$")
|
||||
|
||||
|
||||
def read_front_matter(md_path: Path) -> Tuple[Dict[str, str], int]:
|
||||
"""
|
||||
Returns (meta_dict, end_line_index_exclusive).
|
||||
If no front matter, returns ({}, 0).
|
||||
"""
|
||||
text = md_path.read_text(encoding="utf-8")
|
||||
lines = text.splitlines()
|
||||
|
||||
if not lines or not FRONT_MATTER_RE.match(lines[0]):
|
||||
return {}, 0
|
||||
|
||||
meta: Dict[str, str] = {}
|
||||
i = 1
|
||||
while i < len(lines):
|
||||
if FRONT_MATTER_RE.match(lines[i]):
|
||||
return meta, i + 1
|
||||
|
||||
line = lines[i].strip()
|
||||
i += 1
|
||||
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
|
||||
# Very small YAML subset: key: value
|
||||
if ":" not in line:
|
||||
continue
|
||||
|
||||
key, val = line.split(":", 1)
|
||||
key = key.strip()
|
||||
val = val.strip()
|
||||
|
||||
# Strip optional quotes
|
||||
if (val.startswith('"') and val.endswith('"')) or (val.startswith("'") and val.endswith("'")):
|
||||
val = val[1:-1]
|
||||
|
||||
meta[key] = val
|
||||
|
||||
# Started front matter but never closed
|
||||
raise ValueError(f"{md_path}: front matter starts with '---' but missing closing '---'")
|
||||
|
||||
|
||||
def normalize_id(md_path: Path, meta: Dict[str, str]) -> str:
|
||||
# Prefer explicit id, else derive from filename prefix like "001-something.md"
|
||||
if "id" in meta and meta["id"].strip():
|
||||
return meta["id"].strip()
|
||||
|
||||
m = re.match(r"^(\d+)", md_path.stem)
|
||||
if m:
|
||||
return m.group(1)
|
||||
|
||||
raise ValueError(f"{md_path}: missing 'id' in front matter and filename doesn't start with digits")
|
||||
|
||||
|
||||
def normalize_title(md_path: Path, meta: Dict[str, str], content_start_line: int) -> str:
|
||||
if "title" in meta and meta["title"].strip():
|
||||
return meta["title"].strip()
|
||||
|
||||
# Fallback: first markdown heading after front matter: "# Title"
|
||||
text = md_path.read_text(encoding="utf-8")
|
||||
lines = text.splitlines()
|
||||
for line in lines[content_start_line:]:
|
||||
line = line.strip()
|
||||
if line.startswith("# "):
|
||||
return line[2:].strip()
|
||||
|
||||
raise ValueError(f"{md_path}: missing 'title' in front matter and no '# ' heading found")
|
||||
|
||||
|
||||
def normalize_status(md_path: Path, meta: Dict[str, str]) -> str:
|
||||
status = meta.get("status", "").strip().upper()
|
||||
if status not in ALLOWED_STATUS:
|
||||
raise ValueError(f"{md_path}: invalid or missing 'status' (must be one of: {sorted(ALLOWED_STATUS)})")
|
||||
return status
|
||||
|
||||
|
||||
def normalize_created(md_path: Path, meta: Dict[str, str]) -> str:
|
||||
created = meta.get("created", "").strip()
|
||||
if created:
|
||||
# Validate YYYY-MM-DD
|
||||
try:
|
||||
dt.date.fromisoformat(created)
|
||||
except ValueError:
|
||||
raise ValueError(f"{md_path}: invalid 'created' date '{created}' (expected YYYY-MM-DD)")
|
||||
return created
|
||||
|
||||
# Fallback: file mtime (not perfect, but better than failing)
|
||||
mtime = dt.datetime.fromtimestamp(md_path.stat().st_mtime, tz=dt.timezone.utc).date().isoformat()
|
||||
return mtime
|
||||
|
||||
|
||||
def load_tasks() -> List[TaskMeta]:
|
||||
if not TASKS_DIR.exists():
|
||||
raise FileNotFoundError(f"Tasks directory not found: {TASKS_DIR}")
|
||||
|
||||
md_files = sorted(TASKS_DIR.glob("*.md"))
|
||||
tasks: List[TaskMeta] = []
|
||||
seen_ids: Dict[str, Path] = {}
|
||||
|
||||
for f in md_files:
|
||||
meta, content_start = read_front_matter(f)
|
||||
|
||||
task_id = normalize_id(f, meta)
|
||||
if task_id in seen_ids:
|
||||
raise ValueError(f"Duplicate task id '{task_id}': {seen_ids[task_id]} and {f}")
|
||||
seen_ids[task_id] = f
|
||||
|
||||
title = normalize_title(f, meta, content_start)
|
||||
status = normalize_status(f, meta)
|
||||
created = normalize_created(f, meta)
|
||||
|
||||
tasks.append(TaskMeta(file=f, id=task_id, title=title, status=status, created=created))
|
||||
|
||||
# Sort by numeric id if possible
|
||||
def sort_key(t: TaskMeta):
|
||||
try:
|
||||
return (0, int(t.id))
|
||||
except ValueError:
|
||||
return (1, t.id)
|
||||
|
||||
return sorted(tasks, key=sort_key)
|
||||
|
||||
|
||||
def render_index(tasks: List[TaskMeta]) -> str:
|
||||
lines: List[str] = []
|
||||
lines.append("# Spec Tasks Index")
|
||||
lines.append("")
|
||||
lines.append("> ⚠️ This file is generated. Do not edit manually.")
|
||||
lines.append("")
|
||||
lines.append("## Tasks")
|
||||
lines.append("")
|
||||
lines.append("| ID | Status | Created | Title | File |")
|
||||
lines.append("|---:|:------:|:-------:|:------|:-----|")
|
||||
|
||||
for t in tasks:
|
||||
rel = t.file.relative_to(ROOT).as_posix()
|
||||
title = t.title.replace("|", "\\|")
|
||||
lines.append(f"| {t.id} | {t.status} | {t.created} | {title} | `{rel}` |")
|
||||
|
||||
lines.append("")
|
||||
lines.append("## Summary")
|
||||
lines.append("")
|
||||
todo = sum(1 for t in tasks if t.status == "TODO")
|
||||
done = sum(1 for t in tasks if t.status == "DONE")
|
||||
lines.append(f"- Total: **{len(tasks)}**")
|
||||
lines.append(f"- TODO: **{todo}**")
|
||||
lines.append(f"- DONE: **{done}**")
|
||||
lines.append("")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--check", action="store_true", help="fail if spec/index.md is not up-to-date")
|
||||
args = parser.parse_args()
|
||||
|
||||
tasks = load_tasks()
|
||||
content = render_index(tasks)
|
||||
|
||||
if args.check:
|
||||
existing = INDEX_PATH.read_text(encoding="utf-8") if INDEX_PATH.exists() else ""
|
||||
if existing != content:
|
||||
print("spec/index.md is not up-to-date. Run: python3 spec/gen_spec_index.py", file=sys.stderr)
|
||||
return 2
|
||||
return 0
|
||||
|
||||
SPEC_DIR.mkdir(parents=True, exist_ok=True)
|
||||
INDEX_PATH.write_text(content, encoding="utf-8")
|
||||
print(f"Wrote {INDEX_PATH.relative_to(ROOT)} ({len(tasks)} tasks)")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -1,53 +0,0 @@
|
||||
# Spec Tasks Index
|
||||
|
||||
> ⚠️ This file is generated. Do not edit manually.
|
||||
|
||||
## Tasks
|
||||
|
||||
| ID | Status | Created | Title | File |
|
||||
|---:|:------:|:-------:|:------|:-----|
|
||||
| 000 | DONE | 2026-02-01 | Task format reference | `spec/tasks/000_template.md` |
|
||||
| 001 | DONE | 2026-03-07 | Create app skeleton and component configuration | `spec/tasks/001_create_app_skeleton_and_config.md` |
|
||||
| 002 | DONE | 2026-03-07 | Implement pure domain price rules | `spec/tasks/002_implement_pure_domain_quote_rules.md` |
|
||||
| 003 | DONE | 2026-03-07 | Add CDEK provider adapter | `spec/tasks/003_add_cdek_provider_adapter.md` |
|
||||
| 004 | DONE | 2026-03-07 | Add Redis price cache repository | `spec/tasks/004_add_redis_price_cache_repository.md` |
|
||||
| 005 | DONE | 2026-03-07 | Implement aggregator service workflow | `spec/tasks/005_implement_aggregator_service_workflow.md` |
|
||||
| 006 | DONE | 2026-03-07 | Add delivery price controller endpoint | `spec/tasks/006_add_delivery_price_controller.md` |
|
||||
| 007 | DONE | 2026-03-07 | Add observability correlation and telemetry | `spec/tasks/007_add_observability_correlation.md` |
|
||||
| 008 | DONE | 2026-03-07 | Add SigNoz to Telegram alerting configuration | `spec/tasks/008_add_signoz_telegram_alerting.md` |
|
||||
| 009 | DONE | 2026-03-07 | Add local infrastructure stack | `spec/tasks/009_add_local_infra_stack.md` |
|
||||
| 010 | DONE | 2026-03-08 | Migrate to YAML-only configuration loading | `spec/tasks/010_migrate_to_yaml_only_configuration.md` |
|
||||
| 011 | DONE | 2026-03-08 | Migrate Redis repository client to aioredis | `spec/tasks/011_migrate_redis_repository_to_aioredis.md` |
|
||||
| 012 | DONE | 2026-03-08 | Remove observability and SigNoz stack for phase 1 | `spec/tasks/012_remove_observability_and_signoz_for_phase1.md` |
|
||||
| 013 | DONE | 2026-03-09 | Add configurable provider price multiplier in domain logic | `spec/tasks/013_add_configurable_provider_price_multiplier.md` |
|
||||
| 014 | DONE | 2026-03-12 | Add minimal structlog JSON logging | `spec/tasks/014_add_minimal_structlog_json_logging.md` |
|
||||
| 015 | DONE | 2026-03-13 | Add minimal OpenTelemetry tracing | `spec/tasks/015_add_minimal_opentelemetry_tracing.md` |
|
||||
| 016 | DONE | 2026-03-14 | Add CDEK order registration adapter | `spec/tasks/016_add_cdek_order_registration_adapter.md` |
|
||||
| 017 | DONE | 2026-03-14 | Return all tariffs from CDEK price calculation | `spec/tasks/017_return_all_cdek_tariffs.md` |
|
||||
| 018 | DONE | 2026-03-16 | Add optional parcel type filter to price request | `spec/tasks/018_add_optional_parcel_type_filter_to_price_request.md` |
|
||||
| 019 | DONE | 2026-03-14 | Add CDEK order creation endpoint | `spec/tasks/019_add_cdek_order_creation_endpoint.md` |
|
||||
| 020 | DONE | 2026-03-21 | Rename price request model and use cities_map for CDEK codes | `spec/tasks/020_rename_price_request_model_and_use_cities_map_for_cdek_codes.md` |
|
||||
| 021 | DONE | 2026-03-25 | Add address suggestion adapter and country provider mapping | `spec/tasks/021_add_address_suggestion_adapter_and_country_mapping.md` |
|
||||
| 022 | DONE | 2026-03-25 | Add address suggestion endpoint | `spec/tasks/022_add_address_suggestion_endpoint.md` |
|
||||
| 023 | DONE | 2026-03-29 | Add Yandex Geosuggest address suggestion adapter and CIS routing | `spec/tasks/023_add_yandex_geosuggest_address_suggestion_adapter.md` |
|
||||
| 024 | DONE | 2026-03-29 | Add TomTom address suggestion adapter and Europe routing | `spec/tasks/024_add_tomtom_address_suggestion_adapter.md` |
|
||||
| 025 | DONE | 2026-04-03 | Remove company from Create Delivery Order parties | `spec/tasks/025_remove_company_from_create_delivery_order.md` |
|
||||
| 026 | DONE | 2026-04-05 | Align CDEK order contract with single phone and kilogram package weight | `spec/tasks/026_align_cdek_order_contract_single_phone_and_weight_units.md` |
|
||||
| 027 | DONE | 2026-04-11 | Add TBank payment adapter, init_payment endpoint and rename order flow | `spec/tasks/027_add_tbank_payment_adapter_and_order_payment_link.md` |
|
||||
| 028 | DONE | 2026-04-12 | Add PostgreSQL adapter, order repository and persist order after payment link creation | `spec/tasks/028_add_postgresql_order_persistence.md` |
|
||||
| 029 | DONE | 2026-04-18 | Add TBank payment notification and success URLs | `spec/tasks/029_add_tbank_payment_urls.md` |
|
||||
| 030 | DONE | 2026-04-18 | Add TBank payment notification webhook and CDEK order creation | `spec/tasks/030_add_tbank_payment_notification_webhook.md` |
|
||||
| 031 | TODO | 2026-04-18 | Validate init-payment price with CDEK tariff | `spec/tasks/031_validate_init_payment_price_with_cdek.md` |
|
||||
| 032 | TODO | 2026-05-13 | Rework init-payment contract to camelCase and structured address/contact | `spec/tasks/032_rework_init_payment_contract_camelcase.md` |
|
||||
| 033 | DONE | 2026-05-23 | Add CDEK waybill polling worker | `spec/tasks/033_add_waybill_polling_worker.md` |
|
||||
| 034 | DONE | 2026-05-23 | Add CDEK waybill e-mail sender worker | `spec/tasks/034_add_waybill_email_sender_worker.md` |
|
||||
| 035 | TODO | 2026-05-30 | Generalize tariff_code to string identifier | `spec/tasks/035_generalize_tariff_code_to_string.md` |
|
||||
| 036 | TODO | 2026-05-30 | Add CSE SOAP delivery provider adapter | `spec/tasks/036_add_cse_delivery_provider_adapter.md` |
|
||||
| 037 | TODO | 2026-05-30 | Add CSE geography to cities map | `spec/tasks/037_add_cse_geography_to_cities_map.md` |
|
||||
| 038 | TODO | 2026-05-30 | Route init-payment validation and registration by provider | `spec/tasks/038_route_init_payment_by_provider.md` |
|
||||
|
||||
## Summary
|
||||
|
||||
- Total: **39**
|
||||
- TODO: **6**
|
||||
- DONE: **33**
|
||||
@@ -1,34 +0,0 @@
|
||||
---
|
||||
id: 000
|
||||
title: Task format reference
|
||||
status: DONE
|
||||
created: 2026-02-01
|
||||
---
|
||||
|
||||
## Context
|
||||
В репозитории хранится эталонная задача, которая показывает обязательный формат задач.
|
||||
|
||||
## Goal
|
||||
Дать каноничный пример минимального формата задачи, обязательного для всех файлов в `spec/tasks/`.
|
||||
|
||||
## Constraints
|
||||
- Содержать только требования к формату, без продуктовой реализации.
|
||||
- Оставаться совместимой с обязательной структурой задач Planner.
|
||||
- Не добавлять требований к реализации.
|
||||
|
||||
## Acceptance criteria
|
||||
- Front matter содержит `id`, `title`, `status`, `created`.
|
||||
- Все обязательные секции присутствуют в требуемом порядке.
|
||||
- Содержимое остаётся эталонной, неисполняемой задачей.
|
||||
|
||||
## Definition of Done
|
||||
- [x] Front matter использует обязательные поля.
|
||||
- [x] Обязательные секции присутствуют.
|
||||
- [x] Файл пригоден только как reference формата.
|
||||
|
||||
## Tests
|
||||
- Runtime-тесты не требуются.
|
||||
- Формат валидируется успешной генерацией spec index.
|
||||
|
||||
## Commands
|
||||
- `python3 spec/gen_spec_index.py --check`
|
||||
@@ -1,38 +0,0 @@
|
||||
---
|
||||
id: 001
|
||||
title: Create app skeleton and component configuration
|
||||
status: DONE
|
||||
created: 2026-03-07
|
||||
---
|
||||
|
||||
## Context
|
||||
Сейчас в репозитории есть только specification-файлы, при этом `spec/overview.md` задаёт конкретную структуру приложения и требования к конфигурации компонентов.
|
||||
|
||||
## Goal
|
||||
Создать начальный `app/` skeleton и модели конфигурации из project overview, включая отдельные секции конфигурации для Controller, Service, Business Logic, Repository, Adapter, Observability и Alerts.
|
||||
|
||||
## Constraints
|
||||
- Соблюдать layered architecture из `AGENTS.md`.
|
||||
- Scope задачи: только scaffolding и configuration, без business workflows.
|
||||
- Не реализовывать provider HTTP calls, cache behavior или aggregation logic в рамках этой задачи.
|
||||
- Не изменять файлы в `spec/`.
|
||||
|
||||
## Acceptance criteria
|
||||
- Структура `app/` создана согласно `spec/overview.md`.
|
||||
- `app/config.py` содержит отдельные component configuration sections. Config must be a .yaml-file added to .gitignore.
|
||||
- FastAPI entrypoint существует и успешно импортирует configuration.
|
||||
- В файлах Controller, Service, Repository и Adapter отсутствуют business rules.
|
||||
|
||||
## Definition of Done
|
||||
- [ ] Созданы обязательные директории `app/` и базовые файлы.
|
||||
- [ ] Реализованы раздельные секции конфигурации по компонентам.
|
||||
- [ ] Проходит app import smoke test.
|
||||
- [ ] Изменения остаются строго в scope задачи.
|
||||
|
||||
## Tests
|
||||
- Добавить/import smoke test для FastAPI app startup.
|
||||
- Добавить unit tests, проверяющие загрузку configuration sections из environment variables.
|
||||
|
||||
## Commands
|
||||
- `poetry run pytest tests/smoke/test_app_import.py -q`
|
||||
- `poetry run pytest tests/config/test_config_sections.py -q`
|
||||
@@ -1,37 +0,0 @@
|
||||
---
|
||||
id: 002
|
||||
title: Implement pure domain price rules
|
||||
status: DONE
|
||||
created: 2026-03-07
|
||||
---
|
||||
|
||||
## Context
|
||||
`spec/overview.md` требует business rules для фильтрации тарифов, сравнения цен, сортировки и нормализации входных данных в чистом слое Business Logic.
|
||||
|
||||
## Goal
|
||||
Реализовать детерминированные, dependency-free domain functions в `app/domain/price.py` для нормализации запроса и фильтрации/упорядочивания котировок.
|
||||
|
||||
## Constraints
|
||||
- Business Logic должна быть pure и dependency-free.
|
||||
- Запрещены прямые IO, adapters, repositories, framework imports и внешние вызовы.
|
||||
- Слои Service и Controller не должны забирать domain rules из этой задачи.
|
||||
- Не изменять файлы в `spec/`.
|
||||
|
||||
## Acceptance criteria
|
||||
- Domain functions нормализуют значения запроса по явным детерминированным правилам.
|
||||
- Domain functions отфильтровывают невалидные котировки по определённым domain conditions.
|
||||
- Domain functions сортируют валидные котировки по возрастанию цены.
|
||||
- Логика реализована только в `app/domain/` и доступна для вызова из Service.
|
||||
|
||||
## Definition of Done
|
||||
- [ ] Реализованы pure domain functions для normalization/filtering/sorting.
|
||||
- [ ] Поведение функций детерминированное и без side effects.
|
||||
- [ ] В domain module нет импортов не-domain зависимостей.
|
||||
- [ ] Unit tests (без mocks) покрывают стандартные и edge cases.
|
||||
|
||||
## Tests
|
||||
- Добавить `tests/domain/test_price.py` только с unit tests.
|
||||
- Покрыть edge cases: пустой input, невалидные price, одинаковые цены, границы нормализации.
|
||||
|
||||
## Commands
|
||||
- `poetry run pytest tests/domain/test_price.py -q`
|
||||
@@ -1,43 +0,0 @@
|
||||
---
|
||||
id: 003
|
||||
title: Add CDEK provider adapter
|
||||
status: DONE
|
||||
created: 2026-03-07
|
||||
---
|
||||
|
||||
## Context
|
||||
Phase 1 в `spec/overview.md` требует поддержку provider CDEK с OAuth2 authentication и маппингом расчёта тарифа.
|
||||
|
||||
## Goal
|
||||
Реализовать adapter interfaces и модули CDEK adapter: provider contract, OAuth2 token handling, HTTP client behavior, преобразование ответа в `DeliveryPrice` и чтение параметров CDEK из секции `adapter` YAML-конфига.
|
||||
|
||||
## Constraints
|
||||
- Только Adapter layer; без business decisions и sorting logic.
|
||||
- Внешний IO должен оставаться внутри adapter modules.
|
||||
- Timeout для CDEK должен быть 10 секунд согласно specification.
|
||||
- Параметры CDEK (`base_url`, OAuth2 credentials, retry policy, timeout, cache TTL) должны приходить из секции `adapter` в `.yaml` конфиге.
|
||||
- Запрещён хардкод credentials и runtime-параметров адаптера.
|
||||
- Не изменять файлы в `spec/`.
|
||||
|
||||
## Acceptance criteria
|
||||
- Существует интерфейс `DeliveryProvider` со стабильным контрактом `get_price()`.
|
||||
- Модуль CDEK auth получает OAuth2 token через client credentials и переиспользует валидный token до истечения.
|
||||
- CDEK client выполняет tariff request с timeout и retry behavior.
|
||||
- CDEK mapper преобразует provider response в унифицированную schema `DeliveryPrice`.
|
||||
- Adapter наружу предоставляет только provider-facing API, необходимый Service.
|
||||
- Адаптер использует значения из секции `adapter` YAML-конфига, включая `timeout_seconds=10` и `cache_ttl_seconds=900` по умолчанию.
|
||||
|
||||
## Definition of Done
|
||||
- [ ] Реализован base provider interface.
|
||||
- [ ] Реализованы модули CDEK auth/client/mapper.
|
||||
- [ ] CDEK adapter возвращает унифицированную price model при успешном ответе.
|
||||
- [ ] Adapter tests покрывают auth refresh, timeout/retry и mapping cases.
|
||||
|
||||
## Tests
|
||||
- Добавить adapter unit tests для token lifecycle behavior.
|
||||
- Добавить adapter tests для response mapping и HTTP error handling.
|
||||
- Добавить tests, проверяющие применение параметров CDEK из секции `adapter` YAML-конфига.
|
||||
- Для внешних HTTP взаимодействий использовать stubs/mocks.
|
||||
|
||||
## Commands
|
||||
- `poetry run pytest tests/adapters/delivery_providers/cdek -q`
|
||||
@@ -1,40 +0,0 @@
|
||||
---
|
||||
id: 004
|
||||
title: Add Redis price cache repository
|
||||
status: DONE
|
||||
created: 2026-03-07
|
||||
---
|
||||
|
||||
## Context
|
||||
Product requirements требуют кеширование ответов provider с configurable TTL, а архитектура закрепляет доступ к данным за слоем Repository.
|
||||
|
||||
## Goal
|
||||
Реализовать `PriceCache` repository на Redis с операциями `get`, `set` и `invalidate`, включая поддержку TTL из секции `repository` YAML-конфига.
|
||||
|
||||
## Constraints
|
||||
- Только Repository layer: data access и persistence mapping.
|
||||
- В repository code не допускаются business decisions и workflow orchestration.
|
||||
- Генерация cache key не должна реализовываться в repository.
|
||||
- Параметры Redis и TTL должны читаться из секции `repository` в `.yaml` конфиге.
|
||||
- Не изменять файлы в `spec/`.
|
||||
|
||||
## Acceptance criteria
|
||||
- Repository module существует по пути `app/repositories/cache/redis_cache.py`.
|
||||
- `PriceCache` предоставляет async методы `get`, `set`, `invalidate`.
|
||||
- `set` применяет TTL из configuration.
|
||||
- Serialization/deserialization cached payload выполняются детерминированно.
|
||||
- Repository использует только параметры секции `repository` YAML-конфига для подключения к Redis и настройки TTL.
|
||||
|
||||
## Definition of Done
|
||||
- [ ] Реализован Redis-backed cache repository.
|
||||
- [ ] Интерфейс repository соответствует требуемым операциям.
|
||||
- [ ] Поведение TTL покрыто тестами.
|
||||
- [ ] В repository отсутствует business logic.
|
||||
|
||||
## Tests
|
||||
- Добавить repository tests для cache hit/miss, set/get roundtrip, invalidate и TTL expiration.
|
||||
- Добавить tests, проверяющие чтение TTL и Redis connection settings из секции `repository` YAML-конфига.
|
||||
- Использовать Redis test double или изолированный test Redis instance.
|
||||
|
||||
## Commands
|
||||
- `poetry run pytest tests/repositories/cache/test_redis_cache.py -q`
|
||||
@@ -1,39 +0,0 @@
|
||||
---
|
||||
id: 005
|
||||
title: Implement aggregator service workflow
|
||||
status: DONE
|
||||
created: 2026-03-07
|
||||
---
|
||||
|
||||
## Context
|
||||
`spec/overview.md` определяет `AggregatorService.get_all_prices()` как application orchestrator для параллельных provider calls, graceful degradation, caching и отсортированного унифицированного результата.
|
||||
|
||||
## Goal
|
||||
Реализовать `AggregatorService.get_all_prices(request: DeliveryRequest) -> list[DeliveryPrice]` в `app/services/aggregator.py` с параллельным выполнением, cache coordination и делегированием business rules в domain functions.
|
||||
|
||||
## Constraints
|
||||
- Service выполняет только orchestration; pure business rules остаются в Business Logic.
|
||||
- Использовать dependency injection для providers и repository.
|
||||
- Для вызовов providers использовать `asyncio.gather(..., return_exceptions=True)`.
|
||||
- Не изменять файлы в `spec/`.
|
||||
|
||||
## Acceptance criteria
|
||||
- Service вызывает все зарегистрированные providers параллельно.
|
||||
- Failures/timeouts отдельных providers не валят весь запрос; упавшие providers исключаются.
|
||||
- Cache проверяется до внешнего provider request и обновляется после успешного provider response.
|
||||
- Фильтрация/сортировка котировок делегируется domain functions.
|
||||
- Возвращаемый список котировок унифицирован и отсортирован по цене по возрастанию.
|
||||
|
||||
## Definition of Done
|
||||
- [ ] Метод Service реализован с injected dependencies.
|
||||
- [ ] Параллельная orchestration использует `return_exceptions=True`.
|
||||
- [ ] Реализована cache coordination для provider responses.
|
||||
- [ ] Service tests покрывают full-success, partial-failure, all-failure и cache-hit сценарии.
|
||||
|
||||
## Tests
|
||||
- Добавить service tests со stubs/mocks для adapters и repository.
|
||||
- Проверить orchestration и исключение неуспешных providers.
|
||||
- Проверить делегирование sorting/filtering в domain logic.
|
||||
|
||||
## Commands
|
||||
- `poetry run pytest tests/services/test_aggregator.py -q`
|
||||
@@ -1,37 +0,0 @@
|
||||
---
|
||||
id: 006
|
||||
title: Add delivery price controller endpoint
|
||||
status: DONE
|
||||
created: 2026-03-07
|
||||
---
|
||||
|
||||
## Context
|
||||
API contract требует endpoint `POST /api/v1/delivery/price` с DTO validation и HTTP error mapping, при этом Controller должен вызывать ровно один метод Service.
|
||||
|
||||
## Goal
|
||||
Реализовать request/response schemas и endpoint в `app/controllers/v1/delivery.py`, который валидирует входные данные, делегирует в `AggregatorService.get_all_prices()` и маппит service exceptions в HTTP responses.
|
||||
|
||||
## Constraints
|
||||
- Controller не должен содержать business logic или provider-specific branching.
|
||||
- На каждый запрос Controller должен вызывать ровно один метод Service.
|
||||
- Validation должна использовать Pydantic models из schema layer.
|
||||
- Не изменять файлы в `spec/`.
|
||||
|
||||
## Acceptance criteria
|
||||
- `POST /api/v1/delivery/price` принимает payload `DeliveryRequest` и возвращает `list[DeliveryPrice]`.
|
||||
- Невалидный input возвращает validation error response.
|
||||
- Controller делегирует обработку в `AggregatorService.get_all_prices()`.
|
||||
- Service exceptions маппятся в детерминированные HTTP responses.
|
||||
|
||||
## Definition of Done
|
||||
- [ ] Реализованы request и response schemas.
|
||||
- [ ] Controller endpoint подключён в FastAPI router.
|
||||
- [ ] Path и HTTP method endpoint соответствуют specification.
|
||||
- [ ] API tests покрывают успешный ответ, validation failure и mapped service error.
|
||||
|
||||
## Tests
|
||||
- Добавить controller tests для поведения route и делегирования в service.
|
||||
- Добавить API-level tests для валидации request schema и response schema.
|
||||
|
||||
## Commands
|
||||
- `poetry run pytest tests/controllers/v1/test_delivery.py -q`
|
||||
@@ -1,47 +0,0 @@
|
||||
---
|
||||
id: 007
|
||||
title: Add observability correlation and telemetry
|
||||
status: DONE
|
||||
created: 2026-03-07
|
||||
---
|
||||
|
||||
## Context
|
||||
Проект требует связать traces, metrics и logs через request correlation (`request_id` и `trace_id`) и добавить manual spans вокруг ключевых операций.
|
||||
|
||||
## Goal
|
||||
Реализовать observability wiring: middleware для `request_id`, propagation structured logging context с корреляцией по `trace_id`, OpenTelemetry instrumentation для FastAPI/httpx, manual spans для операций service/provider/cache и метрики из `spec/overview.md`.
|
||||
|
||||
## Constraints
|
||||
- Все observability concerns держать вне Business Logic.
|
||||
- При добавлении telemetry не вводить business decision-making.
|
||||
- Инструментировать только слои и операции, указанные в `spec/overview.md`.
|
||||
- Конфигурация telemetry должна читаться из секции `observability` в `.yaml` конфиге.
|
||||
- Не изменять файлы в `spec/`.
|
||||
|
||||
## Acceptance criteria
|
||||
- Middleware назначает UUID `request_id` для каждого запроса.
|
||||
- `request_id` добавляется в structured logs через contextvars.
|
||||
- Логи коррелируются с трассировкой через `trace_id`.
|
||||
- Включена OpenTelemetry instrumentation для FastAPI и httpx.
|
||||
- Есть manual spans для `AggregatorService.get_all_prices`, provider `get_price` и cache operations.
|
||||
- Span attributes включают `provider`, `from_city`, `to_city`, `weight_kg`, `cache_hit`, `tariffs_found` там, где применимо.
|
||||
- Реализованы и экспортируются метрики: количество запросов, количество ошибок, latency (включая p50/p99), доступность провайдеров.
|
||||
|
||||
## Definition of Done
|
||||
- [ ] Реализован middleware корреляции запросов.
|
||||
- [ ] В logging context есть binding `request_id`.
|
||||
- [ ] Реализована автоматическая и manual tracing instrumentation.
|
||||
- [ ] Реализованы метрики, соответствующие overview.
|
||||
- [ ] Observability tests проверяют создание spans, обязательные attributes и метрики.
|
||||
|
||||
## Tests
|
||||
- Добавить middleware tests для генерации и propagation `request_id`.
|
||||
- Добавить telemetry tests с in-memory span exporter для проверки names/attributes.
|
||||
- Добавить logging tests, проверяющие наличие `request_id` в log context.
|
||||
- Добавить tests для проверки экспорта метрик (request count, error count, latency, provider availability).
|
||||
|
||||
## Commands
|
||||
- `poetry run pytest tests/controllers/test_middleware_request_id.py -q`
|
||||
- `poetry run pytest tests/observability/test_tracing.py -q`
|
||||
- `poetry run pytest tests/observability/test_logging_context.py -q`
|
||||
- `poetry run pytest tests/observability/test_metrics.py -q`
|
||||
@@ -1,41 +0,0 @@
|
||||
---
|
||||
id: 008
|
||||
title: Add SigNoz to Telegram alerting configuration
|
||||
status: DONE
|
||||
created: 2026-03-07
|
||||
---
|
||||
|
||||
## Context
|
||||
`spec/overview.md` требует anomaly alerting через цепочку `SigNoz Alert Rules -> Webhook -> Telegram Bot API` с вынесением token в configuration.
|
||||
|
||||
## Goal
|
||||
Добавить configuration и infrastructure artifacts, необходимые для маршрутизации anomaly alerts из SigNoz в Telegram и для соответствия alert thresholds требованиям проекта через секцию `alerts` в `.yaml` конфиге.
|
||||
|
||||
## Constraints
|
||||
- Alert transport/configuration должны быть отделены от business logic.
|
||||
- Telegram credentials должны приходить из секции `alerts` YAML-конфига и не быть hardcoded.
|
||||
- Thresholds должны соответствовать alert conditions из `spec/overview.md`.
|
||||
- Не изменять файлы в `spec/`.
|
||||
|
||||
## Acceptance criteria
|
||||
- Configuration содержит отдельную секцию `alerts` в `.yaml` конфиге с Telegram settings.
|
||||
- Infrastructure artifact(s) описывают routing от SigNoz alert webhook к Telegram Bot API.
|
||||
- Представлены alert conditions для:
|
||||
- provider 5xx: 5 ошибок за 5 минут
|
||||
- provider p99 latency > 5000ms в течение 10 минут
|
||||
- provider unavailable более 5 минут
|
||||
- Setup instructions содержат пример секции `alerts` YAML-конфига и шаги верификации.
|
||||
|
||||
## Definition of Done
|
||||
- [ ] Реализована schema конфигурации Alerts.
|
||||
- [ ] Добавлены artifact(s) для маршрутизации SigNoz-to-Telegram.
|
||||
- [ ] Определены все три обязательных anomaly conditions.
|
||||
- [ ] Инструкции валидации исполнимы в local environment.
|
||||
|
||||
## Tests
|
||||
- Добавить config tests для проверки загрузки alert settings из секции `alerts` YAML-конфига и обязательных полей.
|
||||
- Добавить tests для alert payload transformation/transport logic, если он реализован в коде.
|
||||
|
||||
## Commands
|
||||
- `poetry run pytest tests/config/test_alerts_config.py -q`
|
||||
- `docker compose config`
|
||||
@@ -1,39 +0,0 @@
|
||||
---
|
||||
id: 009
|
||||
title: Add local infrastructure stack
|
||||
status: DONE
|
||||
created: 2026-03-07
|
||||
---
|
||||
|
||||
## Context
|
||||
Overview задаёт локальные infrastructure components `app`, `redis` и `signoz`, необходимые для runtime и observability.
|
||||
|
||||
## Goal
|
||||
Добавить Docker Compose infrastructure definitions для local development, включая application service, Redis cache и SigNoz observability backend.
|
||||
|
||||
## Constraints
|
||||
- Scope задачи: только infrastructure wiring.
|
||||
- Не переносить business logic в infrastructure scripts.
|
||||
- Имена services должны соответствовать overview: `app`, `redis`, `signoz`.
|
||||
- Не изменять файлы в `spec/`.
|
||||
|
||||
## Acceptance criteria
|
||||
- Compose file определяет services `app`, `redis`, `signoz`.
|
||||
- Service `app` содержит environment wiring для cache и observability endpoints.
|
||||
- Service `redis` достижим из `app` во внутренней сети.
|
||||
- Service `signoz` достижим для telemetry export при локальном запуске.
|
||||
|
||||
## Definition of Done
|
||||
- [ ] Добавлен compose definition с обязательными services.
|
||||
- [ ] Задокументированы environment variables для локальной интеграции.
|
||||
- [ ] `docker compose config` проходит валидацию.
|
||||
- [ ] Добавлены базовые startup instructions.
|
||||
|
||||
## Tests
|
||||
- Добавить/обновить infrastructure checks для валидации compose syntax.
|
||||
- Добавить smoke sequence команд для проверки локального запуска.
|
||||
|
||||
## Commands
|
||||
- `docker compose config`
|
||||
- `docker compose up -d redis signoz`
|
||||
- `docker compose ps`
|
||||
@@ -1,39 +0,0 @@
|
||||
---
|
||||
id: 010
|
||||
title: Migrate to YAML-only configuration loading
|
||||
status: DONE
|
||||
created: 2026-03-08
|
||||
---
|
||||
|
||||
## Context
|
||||
В проекте используется смешанный подход конфигурации с переменными окружения. Требуется унифицировать источник конфигурации и оставить только `config.yaml`.
|
||||
|
||||
## Goal
|
||||
Убрать использование переменных окружения из runtime-конфигурации приложения и перевести загрузку всех параметров на чтение из `config.yaml`.
|
||||
|
||||
## Constraints
|
||||
- Соблюдать layered architecture из `AGENTS.md`.
|
||||
- Scope задачи: только механизм загрузки конфигурации и места её потребления.
|
||||
- Запрещено менять бизнес-правила и поведение use-case, не связанное с конфигурацией.
|
||||
- Не изменять файлы в `spec/`.
|
||||
|
||||
## Acceptance criteria
|
||||
- Runtime-конфигурация приложения загружается только из `config.yaml`.
|
||||
- Переменные окружения не используются для чтения параметров Controller, Service, Business Logic, Repository, Adapter, Observability и Alerts.
|
||||
- При отсутствии обязательных полей в `config.yaml` приложение завершает запуск с детерминированной ошибкой валидации конфигурации.
|
||||
- Тесты конфигурации больше не опираются на `monkeypatch` переменных окружения и проверяют чтение из YAML.
|
||||
|
||||
## Definition of Done
|
||||
- [ ] Удалено чтение переменных окружения из конфигурационного слоя.
|
||||
- [ ] Все компонентные секции конфигурации продолжают читаться из `config.yaml`.
|
||||
- [ ] Обновлены тесты конфигурации под YAML-only поведение.
|
||||
- [ ] Smoke test старта приложения проходит с валидным `config.yaml`.
|
||||
|
||||
## Tests
|
||||
- Обновить `tests/config/test_config_sections.py` под проверку YAML-only загрузки.
|
||||
- Добавить/обновить негативные тесты отсутствующих обязательных YAML-полей.
|
||||
- Проверить app startup smoke test с валидным `config.yaml`.
|
||||
|
||||
## Commands
|
||||
- `poetry run pytest tests/config/test_config_sections.py -q`
|
||||
- `poetry run pytest tests/smoke/test_app_import.py -q`
|
||||
@@ -1,41 +0,0 @@
|
||||
---
|
||||
id: 011
|
||||
title: Migrate Redis repository client to aioredis
|
||||
status: DONE
|
||||
created: 2026-03-08
|
||||
---
|
||||
|
||||
## Context
|
||||
Текущая реализация кеш-репозитория использует клиент `redis`. Требуется перейти на `aioredis` для асинхронного доступа к Redis без изменения бизнес-поведения приложения.
|
||||
|
||||
## Goal
|
||||
Перевести слой Repository (`PriceCache`) и связанные точки инициализации/конфигурации на `aioredis`, сохранив существующий контракт репозитория и runtime-поведение кеша.
|
||||
|
||||
## Constraints
|
||||
- Изменения только в слоях Repository и инициализации зависимостей, необходимых для подключения клиента.
|
||||
- Не изменять бизнес-правила, Controller/API-контракты и оркестрацию Service.
|
||||
- SQL/внешний IO вне Repository и Adapter не добавлять.
|
||||
- Публичный интерфейс `PriceCache` (`get`, `set`, `invalidate`) должен остаться совместимым.
|
||||
- Не изменять файлы в `spec/` кроме этой задачи и сгенерированного `spec/index.md`.
|
||||
|
||||
## Acceptance criteria
|
||||
- В кодовой базе для Redis repository используется `aioredis` вместо `redis`.
|
||||
- Инициализация Redis-клиента и операции `get/set/invalidate` работают асинхронно через `aioredis`.
|
||||
- Поведение TTL и сериализации кеша не изменено относительно текущего контракта.
|
||||
- Конфигурация подключения к Redis продолжает читаться из секции `repository` YAML-конфига.
|
||||
- Ошибки подключения/операций Redis обрабатываются детерминированно в рамках текущего repository-контракта.
|
||||
|
||||
## Definition of Done
|
||||
- [ ] Обновлены зависимости проекта для использования `aioredis`.
|
||||
- [ ] Репозиторий кеша переведен на `aioredis` без изменения публичного интерфейса.
|
||||
- [ ] Обновлены/добавлены тесты репозитория для нового клиента.
|
||||
- [ ] Все релевантные тесты проходят.
|
||||
|
||||
## Tests
|
||||
- Обновить `tests/repositories/cache/test_redis_cache.py` под использование `aioredis`.
|
||||
- Добавить/обновить тесты на cache hit/miss, `set/get` roundtrip, `invalidate`, TTL expiration.
|
||||
- Добавить негативный тест на ошибку Redis-клиента при операции чтения или записи.
|
||||
|
||||
## Commands
|
||||
- `poetry run pytest tests/repositories/cache/test_redis_cache.py -q`
|
||||
- `poetry run pytest tests/services/test_aggregator.py -q`
|
||||
@@ -1,47 +0,0 @@
|
||||
---
|
||||
id: 012
|
||||
title: Remove observability and SigNoz stack for phase 1
|
||||
status: DONE
|
||||
created: 2026-03-08
|
||||
---
|
||||
|
||||
## Context
|
||||
На первом этапе принято решение отказаться от логирования, мониторинга, алертинга и инфраструктуры SigNoz. Текущая кодовая база и тесты содержат эти зависимости и сценарии.
|
||||
|
||||
## Goal
|
||||
Удалить из проекта все runtime- и test-артефакты, связанные с логами, мониторингом, алертами, Telegram alerting и SigNoz, сохранив рабочий API расчета доставки и кеширование.
|
||||
|
||||
## Constraints
|
||||
- Соблюдать layered architecture из `AGENTS.md`; не переносить бизнес-правила между слоями.
|
||||
- Scope задачи ограничен удалением observability/alerting/SigNoz и зависимых конфигураций, инфраструктурных и тестовых артефактов.
|
||||
- Не изменять бизнес-логику расчета тарифов, provider integration и API-контракт `POST /api/v1/delivery/price`.
|
||||
- Не добавлять новую систему мониторинга или алертинга в рамках этой задачи.
|
||||
- Не изменять файлы в `spec/`.
|
||||
|
||||
## Acceptance criteria
|
||||
- Из runtime-приложения удалены middleware, instrumentation и иные механизмы, реализующие request/log correlation, tracing, metrics и alerting.
|
||||
- В коде и конфигурации приложения отсутствуют секции и параметры `observability` и `alerts`, связанные с OpenTelemetry, structlog, SigNoz и Telegram.
|
||||
- Из инфраструктурных файлов удалены сервис/настройки SigNoz и маршрутизация алертов.
|
||||
- Удалены или обновлены тесты observability/alerts; тестовый набор для контроллера, сервиса, репозитория и конфигурации остается зеленым.
|
||||
- По файлам приложения и инфраструктуры нет упоминаний `signoz`, `opentelemetry`, `structlog`, `telegram`.
|
||||
|
||||
## Definition of Done
|
||||
- [ ] Удалены runtime-компоненты observability/alerting.
|
||||
- [ ] Обновлены YAML-конфиги и config schemas без секций observability/alerts.
|
||||
- [ ] Обновлены инфраструктурные файлы без SigNoz.
|
||||
- [ ] Удалены/обновлены тесты observability и alerts.
|
||||
- [ ] Пройдены все команды из раздела Commands.
|
||||
|
||||
## Tests
|
||||
- Обновить `tests/config/test_config_sections.py` под конфигурацию без observability/alerts.
|
||||
- Удалить или заменить `tests/config/test_alerts_config.py` и `tests/observability/*` в соответствии с новым scope.
|
||||
- Обновить `tests/smoke/test_local_infra_stack.py` под инфраструктуру без SigNoz.
|
||||
- Подтвердить, что `tests/controllers/v1/test_delivery.py`, `tests/services/test_aggregator.py`, `tests/repositories/cache/test_redis_cache.py` проходят без observability-зависимостей.
|
||||
|
||||
## Commands
|
||||
- `poetry run pytest tests/controllers/v1/test_delivery.py -q`
|
||||
- `poetry run pytest tests/services/test_aggregator.py -q`
|
||||
- `poetry run pytest tests/repositories/cache/test_redis_cache.py -q`
|
||||
- `poetry run pytest tests/config/test_config_sections.py -q`
|
||||
- `poetry run pytest tests/smoke/test_local_infra_stack.py -q`
|
||||
- `! rg -n "(signoz|opentelemetry|structlog|telegram)" app tests docker-compose.yml pyproject.toml config.yaml config.example.yaml config.test.yaml infra`
|
||||
@@ -1,47 +0,0 @@
|
||||
---
|
||||
id: 013
|
||||
title: Add configurable provider price multiplier in domain logic
|
||||
status: DONE
|
||||
created: 2026-03-09
|
||||
---
|
||||
|
||||
## Context
|
||||
Сейчас `app/domain/price.py` валидирует и сортирует тарифы провайдеров без дополнительной постобработки цены. Появилось новое требование: перед возвратом результата применять к цене провайдера конфигурируемый multiplier из YAML-конфига и округлять итог до целого числа.
|
||||
|
||||
## Goal
|
||||
Добавить в Business Logic детерминированное правило пересчёта `DeliveryPrice.price` через multiplier из секции `business_logic` и провести это значение через конфигурацию и service orchestration без переноса формулы в Service или Adapter.
|
||||
|
||||
## Constraints
|
||||
- Соблюдать layered architecture из `AGENTS.md`.
|
||||
- Формула умножения и округления должна жить только в `app/domain/`; Service может только передавать нужный параметр и делегировать вызов.
|
||||
- Значение multiplier должно читаться из YAML-конфига через секцию `business_logic`.
|
||||
- После применения multiplier цена должна оставаться `Decimal`, но иметь целочисленное значение.
|
||||
- Правило округления должно быть явным и детерминированным: до `Decimal("1")` c `ROUND_HALF_UP`.
|
||||
- Не изменять provider adapters, внешние HTTP-контракты и формат ответа кроме нового значения `price`.
|
||||
- Не изменять файлы в `spec/`.
|
||||
|
||||
## Acceptance criteria
|
||||
- В `business_logic` configuration section добавлено обязательное поле для multiplier цены провайдера с валидацией положительного значения.
|
||||
- Domain functions применяют multiplier к каждой валидной цене провайдера до сортировки результата.
|
||||
- Итоговая цена после умножения округляется до целого значения по правилу `ROUND_HALF_UP`.
|
||||
- Service не содержит формулу расчёта и по-прежнему делегирует обработку цен в domain logic.
|
||||
- Поведение одинаково для свежих ответов провайдера и для cache hit: в обоих случаях наружу возвращается уже пересчитанная цена.
|
||||
|
||||
## Definition of Done
|
||||
- [ ] Обновлена конфигурация `business_logic` и YAML fixtures/example configs для multiplier.
|
||||
- [ ] Реализована pure domain logic для пересчёта и округления цены.
|
||||
- [ ] Service wiring передаёт multiplier в domain logic без добавления business rules в Service.
|
||||
- [ ] Unit/service/config tests покрывают стандартные и edge-case сценарии.
|
||||
|
||||
## Tests
|
||||
- Обновить `tests/domain/test_price.py` для проверки multiplier, округления `ROUND_HALF_UP`, пустого input и невалидных цен.
|
||||
- Обновить `tests/services/test_aggregator.py` для проверки делегирования multiplier и одинакового поведения fresh/cache hit.
|
||||
- Обновить `tests/config/test_config_sections.py` для проверки чтения multiplier из YAML и валидации невалидных значений.
|
||||
- При необходимости обновить `tests/smoke/test_app_import.py` под обязательное новое поле конфигурации.
|
||||
|
||||
## Commands
|
||||
- `poetry run pytest tests/domain/test_price.py -q`
|
||||
- `poetry run pytest tests/services/test_aggregator.py -q`
|
||||
- `poetry run pytest tests/config/test_config_sections.py -q`
|
||||
- `poetry run pytest tests/smoke/test_app_import.py -q`
|
||||
- `python3 spec/gen_spec_index.py --check`
|
||||
@@ -1,44 +0,0 @@
|
||||
---
|
||||
id: 014
|
||||
title: Add minimal structlog JSON logging
|
||||
status: DONE
|
||||
created: 2026-03-12
|
||||
---
|
||||
|
||||
## Context
|
||||
После удаления observability stack приложение осталось без централизованной конфигурации логирования. При этом в runtime-коде есть локальная настройка `logging.basicConfig(...)`, а новое требование состоит в том, чтобы все runtime-логи приложения выводились в JSON без возврата tracing, metrics и alerting.
|
||||
|
||||
## Goal
|
||||
Подключить минимальную централизованную настройку `structlog`, чтобы runtime-логи приложения и Uvicorn выводились как JSON-объекты, а прикладные модули использовали общий logging setup вместо локальной конфигурации.
|
||||
|
||||
## Constraints
|
||||
- Соблюдать layered architecture из `AGENTS.md`.
|
||||
- Scope задачи: только logging wiring, JSON formatting и интеграция существующих logger calls.
|
||||
- `structlog` использовать только для JSON logging; не добавлять `request_id`, `trace_id`, OpenTelemetry, metrics, SigNoz, Telegram и иные observability features.
|
||||
- Конфигурация логирования должна быть централизована на уровне app startup или отдельного runtime-модуля; в Controller, Service, Repository, Adapter и Business Logic запрещено вызывать `logging.basicConfig(...)`.
|
||||
- Не изменять API contract, business rules, provider protocol, cache behavior и логику обработки ошибок.
|
||||
- используй context7, чтобы узнать контракт текущей версии пакета structlog
|
||||
|
||||
## Acceptance criteria
|
||||
- При старте приложения выполняется единая инициализация logging на базе `structlog`.
|
||||
- Логи приложения и `uvicorn.error`/`uvicorn.access` сериализуются в JSON, одна запись на строку.
|
||||
- Каждая лог-запись содержит как минимум поля `event`, `level`, `timestamp` и `logger`.
|
||||
- В прикладных модулях отсутствуют локальные вызовы `logging.basicConfig(...)` и текстовые formatter-конфигурации для runtime logging.
|
||||
- Добавлен тест, который валидирует emitted log line как корректный JSON и проверяет обязательные поля.
|
||||
|
||||
## Definition of Done
|
||||
- [ ] Добавлена централизованная конфигурация JSON logging на базе `structlog`.
|
||||
- [ ] Удалены локальные настройки logging из прикладных модулей.
|
||||
- [ ] `uvicorn.error` и `uvicorn.access` подключены к тому же JSON logging setup.
|
||||
- [ ] Добавлены или обновлены тесты для logging bootstrap и JSON serialization.
|
||||
- [ ] Пройдены все команды из раздела Commands.
|
||||
|
||||
## Tests
|
||||
- Добавить `tests/logging/test_json_logging.py` для проверки JSON serialization и обязательных полей лог-записи.
|
||||
- Обновить `tests/smoke/test_app_import.py` для проверки вызова централизованного logging bootstrap при создании app.
|
||||
- При изменении конфигурации Uvicorn loggers добавить тест на `uvicorn.error` и `uvicorn.access` без запуска реального сервера.
|
||||
|
||||
## Commands
|
||||
- `poetry run pytest tests/logging/test_json_logging.py -q`
|
||||
- `poetry run pytest tests/smoke/test_app_import.py -q`
|
||||
- `poetry run pytest tests/adapters/delivery_providers/cdek/test_client.py -q`
|
||||
@@ -1,50 +0,0 @@
|
||||
---
|
||||
id: 015
|
||||
title: Add minimal OpenTelemetry tracing
|
||||
status: DONE
|
||||
created: 2026-03-13
|
||||
---
|
||||
|
||||
## Context
|
||||
После удаления observability stack в задаче `012` runtime-приложение осталось без трассировки. Появилось новое требование: вернуть только минимальный tracing для входящих HTTP-запросов и исходящих вызовов провайдера/Redis, без возврата metrics, alerting и trace/log correlation.
|
||||
|
||||
## Goal
|
||||
Подключить минимальный OpenTelemetry tracing через централизованный runtime bootstrap: настроить `TracerProvider` и OTLP exporter из YAML-конфига, добавить instrumentation для FastAPI, httpx и Redis и встроить этот bootstrap в startup приложения без переноса telemetry concern в Business Logic.
|
||||
|
||||
## Constraints
|
||||
- Соблюдать layered architecture из `AGENTS.md`.
|
||||
- Scope задачи: только tracing bootstrap, YAML configuration, instrumentation wiring и тесты на это поведение.
|
||||
- Не добавлять metrics, request correlation middleware, `request_id`, `trace_id` в логи, SigNoz, Telegram alerting и иные observability features кроме tracing.
|
||||
- Не добавлять business rules, branching или data access в runtime tracing bootstrap.
|
||||
- Инициализация OpenTelemetry должна жить в отдельном runtime-модуле или app startup; Controller, Service, Repository, Adapter и Business Logic не должны вручную создавать exporter/provider.
|
||||
- Конфигурация tracing должна читаться из отдельной секции `observability` в YAML-файлах конфигурации.
|
||||
- Bootstrap tracing должен быть идемпотентным: повторные вызовы `create_app()` в тестах не должны дублировать global instrumentation и span processors.
|
||||
- Не изменять API contract, логику расчета тарифов, кэш semantics и provider protocol.
|
||||
- Не изменять файлы в `spec/`.
|
||||
|
||||
## Acceptance criteria
|
||||
- В конфигурации приложения добавлена секция `observability` с параметрами включения tracing, `service_name`, OTLP endpoint и флагом insecure transport.
|
||||
- При `observability.enabled = true` приложение на старте создает `TracerProvider` с `Resource(service.name=...)` и OTLP exporter, используя значения из YAML-конфига.
|
||||
- При `observability.enabled = true` централизованно включается instrumentation для FastAPI, httpx и Redis.
|
||||
- При `observability.enabled = false` приложение стартует без инициализации exporter/provider и без регистрации instrumentation.
|
||||
- Повторное создание приложения или повторный вызов tracing bootstrap не приводит к duplicate instrumentation/span processor registration.
|
||||
- Реализация не восстанавливает metrics, alerts, request/log correlation и не меняет JSON logging contract.
|
||||
|
||||
## Definition of Done
|
||||
- [ ] Добавлена секция `observability` в config schema и runtime YAML-файлы.
|
||||
- [ ] Реализован централизованный runtime bootstrap для OpenTelemetry tracing.
|
||||
- [ ] `create_app()` подключает tracing bootstrap без бизнес-логики и без дублирования инициализации.
|
||||
- [ ] Добавлены tests на конфигурацию, enabled/disabled режимы и идемпотентность tracing setup.
|
||||
- [ ] Добавлен tracing test, подтверждающий создание spans для FastAPI/httpx/Redis instrumentation или корректный wiring этих instrumentors.
|
||||
|
||||
## Tests
|
||||
- Обновить `tests/config/test_config_sections.py` для проверки секции `observability`, обязательных полей и disabled/enabled конфигурации.
|
||||
- Обновить `tests/smoke/test_app_import.py` для проверки вызова централизованного tracing bootstrap при создании app.
|
||||
- Добавить `tests/tracing/test_bootstrap.py` для проверки OTLP exporter/provider setup, `service.name`, enabled/disabled режимов и идемпотентности instrumentation.
|
||||
- Добавить tracing test с in-memory exporter или эквивалентным deterministic harness для проверки, что FastAPI/httpx/Redis instrumentation корректно подключается без запуска внешнего OTLP collector.
|
||||
|
||||
## Commands
|
||||
- `poetry run pytest tests/config/test_config_sections.py -q`
|
||||
- `poetry run pytest tests/smoke/test_app_import.py -q`
|
||||
- `poetry run pytest tests/tracing/test_bootstrap.py -q`
|
||||
- `python3 spec/gen_spec_index.py --check`
|
||||
@@ -1,43 +0,0 @@
|
||||
---
|
||||
id: 016
|
||||
title: Add CDEK order registration adapter
|
||||
status: DONE
|
||||
created: 2026-03-14
|
||||
---
|
||||
|
||||
## Context
|
||||
Сейчас CDEK adapter поддерживает только расчёт тарифа. Новый сценарий требует регистрацию заказа в CDEK по контракту из `http-client.http` в разделе `Регистрация заказа (тип "доставка", до двери)`.
|
||||
|
||||
## Goal
|
||||
Расширить существующий CDEK adapter регистрацией заказа: добавить в текущий adapter/provider сбор payload по контракту, вызов `POST /v2/orders`, детерминированную обработку provider errors и маппинг успешного ответа во внутреннюю response model.
|
||||
|
||||
## Constraints
|
||||
- Изменения ограничены существующим Adapter layer и schema/mapper моделями, необходимыми для стабильного adapter contract.
|
||||
- Использовать существующий OAuth2 flow CDEK; не дублировать auth logic.
|
||||
- Контракт запроса должен соответствовать разделу `Регистрация заказа (тип "доставка", до двери)` из `http-client.http`.
|
||||
- В scope этой задачи входят только значения `type=2` и `tariff_code=535`; не расширять поддержку на другие типы заказа и тарифы.
|
||||
- Не добавлять новые controller/service модули, controller routing, service orchestration, cache behavior и business logic.
|
||||
- Не изменять файлы в `spec/`.
|
||||
|
||||
## Acceptance criteria
|
||||
- Существующий CDEK adapter/provider предоставляет стабильный метод регистрации заказа, принимающий валидированную internal model вместо raw dict.
|
||||
- Adapter отправляет `POST /v2/orders` с bearer token и JSON payload, соответствующим контракту из `http-client.http`.
|
||||
- Успешный ответ CDEK маппится во внутреннюю response model с `order_uuid`, полученным из `entity.uuid`.
|
||||
- Provider validation errors класса 4xx маппятся в детерминированную provider request error, а transport/5xx ошибки — в adapter client error.
|
||||
- Adapter tests покрывают success case, payload mapping, response mapping и error handling для order registration.
|
||||
|
||||
## Definition of Done
|
||||
- [ ] Расширен существующий adapter method set для регистрации заказа CDEK.
|
||||
- [ ] Реализованы request/response mapper(s) для order registration.
|
||||
- [ ] Переиспользуется существующий OAuth2 auth client.
|
||||
- [ ] Добавлены adapter tests для order registration сценария.
|
||||
|
||||
## Tests
|
||||
- Добавить `tests/adapters/delivery_providers/cdek/test_order_client.py` для success/error сценариев регистрации заказа.
|
||||
- Проверить, что payload содержит поля из контракта `http-client.http`.
|
||||
- Проверить маппинг `entity.uuid` в внутреннюю response model.
|
||||
- Для внешних HTTP взаимодействий использовать stubs/mocks.
|
||||
|
||||
## Commands
|
||||
- `poetry run pytest tests/adapters/delivery_providers/cdek/test_order_client.py -q`
|
||||
- `python3 spec/gen_spec_index.py --check`
|
||||
@@ -1,49 +0,0 @@
|
||||
---
|
||||
id: 017
|
||||
title: Return all tariffs from CDEK price calculation
|
||||
status: DONE
|
||||
created: 2026-03-14
|
||||
---
|
||||
|
||||
## Context
|
||||
Сейчас `POST /api/v1/delivery/price` для CDEK возвращает только один тариф, хотя продуктовый контракт требует возвращать унифицированный список тарифов. Текущая реализация adapter/service использует однокотировочный provider contract и CDEK mapper берёт только первый элемент `tariff_codes[0]`.
|
||||
|
||||
## Goal
|
||||
Изменить price flow так, чтобы CDEK adapter возвращал все тарифы из успешного ответа CDEK, а service агрегировал и сортировал объединённый список тарифов без изменения архитектурных границ слоёв.
|
||||
|
||||
## Constraints
|
||||
- Scope задачи ограничен расчётом стоимости доставки и CDEK tariff list response; не изменять order creation flow.
|
||||
- Controller должен сохранить текущий публичный endpoint `POST /api/v1/delivery/price` и по-прежнему вызывать ровно один метод Service.
|
||||
- Service остаётся orchestration layer: собирает тарифы от провайдеров, работает с cache и делегирует фильтрацию/сортировку в Business Logic.
|
||||
- Pure business rules не переносить в Service или Adapter.
|
||||
- Изменение должно затронуть provider contract так, чтобы один provider мог вернуть несколько тарифов в одном запросе.
|
||||
- Scope задачи не включает добавление новых провайдеров, новых endpoint'ов и расширение response schema beyond текущего `DeliveryPrice`, если это не требуется для возврата всех тарифов.
|
||||
- Не изменять файлы в `spec/`.
|
||||
|
||||
## Acceptance criteria
|
||||
- Интерфейс `DeliveryProvider` поддерживает возврат списка тарифов для одного provider request.
|
||||
- CDEK mapper преобразует все элементы `tariff_codes` из валидного ответа CDEK в список `DeliveryPrice`, а не только первый тариф.
|
||||
- CDEK provider adapter возвращает service полный список тарифов, полученный из ответа CDEK.
|
||||
- `AggregatorService.get_all_prices()` корректно обрабатывает список тарифов от каждого provider, объединяет их в единый список и передаёт его в domain filtering/sorting.
|
||||
- Cache coordination в service поддерживает сохранение и чтение списка тарифов для provider request.
|
||||
- `POST /api/v1/delivery/price` возвращает все тарифы CDEK в унифицированном формате, отсортированные по цене по возрастанию.
|
||||
- Existing graceful degradation сохраняется: failure одного provider исключает только его тарифы и не ломает успешные результаты других providers.
|
||||
|
||||
## Definition of Done
|
||||
- [ ] Обновлён provider contract для возврата списка тарифов.
|
||||
- [ ] Обновлены CDEK mapper и adapter/client для многотарифного ответа.
|
||||
- [ ] Обновлён `AggregatorService.get_all_prices()` для объединения и кеширования списков тарифов.
|
||||
- [ ] Добавлены или обновлены tests для adapter, service и controller/API сценариев многотарифного ответа.
|
||||
|
||||
## Tests
|
||||
- Обновить `tests/adapters/delivery_providers/cdek/test_mapper.py` для проверки mapping всех тарифов из `tariff_codes`.
|
||||
- Обновить `tests/adapters/delivery_providers/cdek/test_client.py` для проверки, что provider возвращает список тарифов, а не один объект.
|
||||
- Обновить `tests/services/test_aggregator.py` для flattening provider results, cache hit/miss со списком тарифов и partial failure сценариев.
|
||||
- Обновить `tests/controllers/v1/test_delivery.py` для проверки, что API возвращает несколько тарифов CDEK в одном ответе.
|
||||
|
||||
## Commands
|
||||
- `poetry run pytest tests/adapters/delivery_providers/cdek/test_mapper.py -q`
|
||||
- `poetry run pytest tests/adapters/delivery_providers/cdek/test_client.py -q`
|
||||
- `poetry run pytest tests/services/test_aggregator.py -q`
|
||||
- `poetry run pytest tests/controllers/v1/test_delivery.py -q`
|
||||
- `python3 spec/gen_spec_index.py --check`
|
||||
@@ -1,49 +0,0 @@
|
||||
---
|
||||
id: 018
|
||||
title: Add optional parcel type filter to price request
|
||||
status: DONE
|
||||
created: 2026-03-16
|
||||
---
|
||||
|
||||
## Context
|
||||
Сейчас `POST /api/v1/delivery/price` возвращает все доступные тарифы без возможности отфильтровать их по типу отправления. Новый пользовательский сценарий требует опциональный параметр `parcel_type`, который должен фильтровать уже агрегированный список тарифов для всех провайдеров.
|
||||
|
||||
## Goal
|
||||
Расширить контракт `DeliveryRequest` опциональным полем `parcel_type` и добавить детерминированную фильтрацию тарифов в price flow по правилам `doc` и `parcel` без изменения публичного path endpoint.
|
||||
|
||||
## Constraints
|
||||
- `parcel_type` является необязательным полем запроса `POST /api/v1/delivery/price`.
|
||||
- Допустимые значения ограничены `doc` и `parcel`; другие значения должны отклоняться schema validation.
|
||||
- Правило фильтрации является pure business rule и должно жить в `app/domain/`; Controller только валидирует DTO, Service только оркестрирует и передаёт параметр в domain logic.
|
||||
- Фильтрация должна применяться к унифицированному списку тарифов для всех провайдеров и не должна требовать provider-specific branching или изменения provider transport contract.
|
||||
- Для `doc` включаются только тарифы, у которых `service_name` содержит `документ` или `document` без учёта регистра.
|
||||
- Для `parcel` возвращаются все остальные тарифы, не попавшие под правило `doc`.
|
||||
- Если `parcel_type` не передан, поведение endpoint остаётся прежним: возвращаются все тарифы.
|
||||
- Scope задачи не включает изменение order flow, добавление новых endpoint'ов, расширение набора значений `parcel_type` и иные изменения вне price flow.
|
||||
- Не изменять файлы в `spec/`.
|
||||
|
||||
## Acceptance criteria
|
||||
- `DeliveryRequest` поддерживает опциональное поле `parcel_type`.
|
||||
- `POST /api/v1/delivery/price` принимает запросы без `parcel_type` и возвращает полный список тарифов без дополнительной фильтрации.
|
||||
- При `parcel_type=doc` ответ содержит только тарифы, у которых `service_name` содержит `документ` или `document` без учёта регистра.
|
||||
- При `parcel_type=parcel` ответ содержит только тарифы, у которых `service_name` не содержит `документ` и `document` без учёта регистра.
|
||||
- Невалидное значение `parcel_type` приводит к 422 response на уровне schema validation.
|
||||
- Фильтрация работает одинаково для тарифов, полученных из provider responses и из cache.
|
||||
|
||||
## Definition of Done
|
||||
- [ ] Обновлён контракт price request с опциональным `parcel_type`.
|
||||
- [ ] Реализована pure domain logic для фильтрации тарифов по `parcel_type`.
|
||||
- [ ] Service wiring передаёт `parcel_type` в domain logic без добавления business rules в Service.
|
||||
- [ ] Controller/API tests покрывают сценарии `doc`, `parcel`, отсутствие параметра и невалидное значение.
|
||||
- [ ] Поведение одинаково для fresh provider results и cache hit.
|
||||
|
||||
## Tests
|
||||
- Обновить `tests/domain/test_price.py` для проверки case-insensitive фильтрации по `документ` и `document`, а также поведения без `parcel_type`.
|
||||
- Обновить `tests/services/test_aggregator.py` для проверки делегирования `parcel_type` в domain logic и одинакового результата для fresh path и cache hit.
|
||||
- Обновить `tests/controllers/v1/test_delivery.py` для проверки optional request field, 422 на невалидное значение и response filtering для `doc` и `parcel`.
|
||||
|
||||
## Commands
|
||||
- `poetry run pytest tests/domain/test_price.py -q`
|
||||
- `poetry run pytest tests/services/test_aggregator.py -q`
|
||||
- `poetry run pytest tests/controllers/v1/test_delivery.py -q`
|
||||
- `python3 spec/gen_spec_index.py --check`
|
||||
@@ -1,53 +0,0 @@
|
||||
---
|
||||
id: 019
|
||||
title: Add CDEK order creation endpoint
|
||||
status: DONE
|
||||
created: 2026-03-14
|
||||
---
|
||||
|
||||
## Context
|
||||
Сейчас order flow должен принимать уже готовые точные адреса в полях `from_location.address` и `to_location.address`. Для нового пользовательского сценария нужен отдельный endpoint `/order`, который создаёт заказ в CDEK по контракту из `http-client.http`, не выполняя address suggestion lookup внутри order flow.
|
||||
|
||||
## Goal
|
||||
Добавить `POST /api/v1/delivery/order` в существующий controller и существующий service с request/response schemas и error mapping для регистрации заказа в CDEK через adapter contract из задачи `016`.
|
||||
|
||||
## Constraints
|
||||
- Controller отвечает только за DTO validation, routing и mapping service exceptions в HTTP responses.
|
||||
- Endpoint должен вызывать ровно один метод Service: `AggregatorService.create_order()`.
|
||||
- Новый endpoint должен быть добавлен в существующий controller модуль `app/controllers/v1/delivery.py`; не создавать отдельный controller модуль.
|
||||
- Логика создания заказа должна быть добавлена в существующий service модуль `app/services/aggregator.py`; не создавать отдельный service модуль.
|
||||
- Service оркестрирует только вызов injected CDEK order adapter и не содержит business logic или provider HTTP-деталей.
|
||||
- `from_location.address` и `to_location.address` считаются уже выбранными точными строками адреса; в рамках этой задачи запрещено добавлять address suggestion routing, внешние address lookup вызовы и нормализацию адреса.
|
||||
- Контракт входного запроса должен соответствовать разделу `Регистрация заказа (тип "доставка", до двери)` из `http-client.http`.
|
||||
- В scope задачи входят только значения `type=2` и `tariff_code=535`; не расширять поддержку на другие типы заказа и тарифы.
|
||||
- Scope задачи не включает `POST /api/v1/delivery/suggest-address`, конфигурацию address suggestion providers, кеширование, агрегацию тарифов, расчёт стоимости, новые провайдеры и расширение order flow за пределы CDEK.
|
||||
- Не изменять файлы в `spec/`.
|
||||
|
||||
## Acceptance criteria
|
||||
- Существует endpoint `POST /api/v1/delivery/order`, принимающий payload по контракту из `http-client.http`.
|
||||
- Реализованы request/response schemas `OrderCreateRequest` и `OrderCreateResponse` для создания заказа.
|
||||
- Endpoint реализован в существующем controller `app/controllers/v1/delivery.py`.
|
||||
- Controller делегирует обработку только в `AggregatorService.create_order()`.
|
||||
- Service вызывает injected adapter для регистрации заказа и возвращает `OrderCreateResponse` с `provider` и `order_uuid`.
|
||||
- Логика orchestration размещена в существующем service `app/services/aggregator.py`.
|
||||
- Endpoint принимает значения `from_location.address` и `to_location.address` как opaque input strings и не выполняет address suggestion lookup перед вызовом adapter.
|
||||
- Ошибки валидации входного payload возвращают 422, provider request errors маппятся в 400, недоступность CDEK и transport failures — в 503.
|
||||
- API и service tests покрывают success case и основные failure scenarios.
|
||||
|
||||
## Definition of Done
|
||||
- [ ] Добавлены order request/response schemas.
|
||||
- [ ] Реализован метод `AggregatorService.create_order()` для оркестрации регистрации заказа.
|
||||
- [ ] Реализован endpoint `POST /api/v1/delivery/order` в существующем controller.
|
||||
- [ ] Добавлены service и controller/API tests для order creation flow.
|
||||
|
||||
## Tests
|
||||
- Добавить `tests/services/test_order.py` для проверки вызова adapter и маппинга ошибок сервиса.
|
||||
- Добавить `tests/controllers/v1/test_order.py` для success case, schema validation и HTTP mapping ошибок.
|
||||
- При необходимости обновить `tests/smoke/test_app_import.py` для проверки подключения нового endpoint и service wiring без новых модулей.
|
||||
- Использовать test doubles для adapter dependency.
|
||||
|
||||
## Commands
|
||||
- `poetry run pytest tests/services/test_order.py -q`
|
||||
- `poetry run pytest tests/controllers/v1/test_order.py -q`
|
||||
- `poetry run pytest tests/smoke/test_app_import.py -q`
|
||||
- `python3 spec/gen_spec_index.py --check`
|
||||
@@ -1,58 +0,0 @@
|
||||
---
|
||||
id: 020
|
||||
title: Rename price request model and use cities_map for CDEK codes
|
||||
status: DONE
|
||||
created: 2026-03-21
|
||||
---
|
||||
|
||||
## Context
|
||||
Текущий price flow принимает `DeliveryRequest` со строковыми `from_city` и `to_city`, а также использует внешний lookup города через `_resolve_city_code()` в CDEK client. Новый контракт API переходит на идентификаторы городов и локальный справочник `cities_map`, который хранит provider-specific данные города. Для этого изменения нужен только минимальный набор новых валидаций, необходимый для корректного price flow.
|
||||
|
||||
## Goal
|
||||
Переименовать request model расчёта доставки в `DeliveryCalculationRequest`, перевести поля `from_city` и `to_city` на тип `int`, убрать `country_code` из price API и использовать `cities_map` как источник provider city codes для CDEK tariff request вместо `_resolve_city_code()`, добавив только минимально необходимые валидации для этого перехода.
|
||||
|
||||
## Constraints
|
||||
- Scope задачи ограничен price calculation flow: request schema, controller/service wiring, domain normalization, cache key composition, provider contract и CDEK adapter/client.
|
||||
- `POST /api/v1/delivery/order`, `OrderCreateRequest`, `OrderCreateResponse` и `country_code` внутри order payload не изменять.
|
||||
- Controller не добавляет business rules; Service не реализует provider-specific lookup; pure validation и normalization остаются в Business Logic.
|
||||
- Для расчёта CDEK использовать только локальный `cities_map`; внешний city lookup через CDEK API и `_resolve_city_code()` должен быть удалён из client.
|
||||
- Не требовать полноты `cities_map`; отсутствие записи города или provider-specific данных должно обрабатываться детерминированно.
|
||||
- Учитывать provider-specific структуру `cities_map`, но не реализовывать новых providers в рамках этой задачи.
|
||||
- Новые проверки ограничить минимально необходимыми для работы контракта и CDEK lookup: тип city identifier во входной schema и наличие валидного `cdek.code` для используемого города.
|
||||
- Не добавлять предварительную валидацию всего `cities_map`, проверку неиспользуемых provider sections, проверку `city_uuid`, `full_name`, `label`, `country` или иные дополнительные consistency-checks.
|
||||
- Не добавлять новые range/business validations для city identifier сверх тех, что нужны для поиска города в `cities_map`.
|
||||
- Не изменять файлы в `spec/`, кроме этой задачи, `spec/overview.md` и сгенерированного `spec/index.md`.
|
||||
|
||||
## Acceptance criteria
|
||||
- Модель запроса расчёта переименована в `DeliveryCalculationRequest`; controller, service и provider contract для price flow используют новое имя.
|
||||
- Поля `from_city` и `to_city` в price request принимают целочисленные идентификаторы города; поле `country_code` отсутствует в публичном API расчёта доставки.
|
||||
- Domain normalization и service cache key больше не используют `country_code` и корректно работают с city identifiers.
|
||||
- Новые валидации, добавленные этой задачей, ограничены:
|
||||
- schema-level проверкой, что `from_city` и `to_city` передаются как `int`
|
||||
- runtime-проверкой, что для конкретного city identifier, использованного в CDEK price flow, существует валидный `cdek.code`
|
||||
- CDEK client строит payload `from_location.code` и `to_location.code` на основе `cities_map` и значений `cdek.code` для переданных city identifiers.
|
||||
- Если city identifier отсутствует в `cities_map` или для него нет валидного `cdek.code`, CDEK price flow завершается детерминированной provider request error без внешнего city lookup.
|
||||
- Метод `_resolve_city_code()` и связанный HTTP lookup `location/suggest/cities` удалены из CDEK client; price flow не обращается к CDEK API подсказок городов.
|
||||
- Existing order creation flow и order schemas остаются без изменения.
|
||||
|
||||
## Definition of Done
|
||||
- [ ] Обновлены request schema, controller/service/provider type hints и связанные импорты для нового имени `DeliveryCalculationRequest`.
|
||||
- [ ] Удалён `country_code` из price calculation contract, normalization и cache key.
|
||||
- [ ] Реализован lookup CDEK city codes через `cities_map` без внешнего city suggest API.
|
||||
- [ ] Удалён `_resolve_city_code()` и обновлены adapter tests под новое поведение.
|
||||
- [ ] Добавлены или обновлены tests только для минимально необходимой schema validation, service wiring, cache key и CDEK payload mapping/error scenarios.
|
||||
|
||||
## Tests
|
||||
- Обновить `tests/domain/test_price.py` только для проверки, что normalizer корректно принимает и сохраняет city identifiers без `country_code`.
|
||||
- Обновить `tests/services/test_aggregator.py` для проверки нового request model, отсутствия `country_code` в service flow и обновлённого cache key.
|
||||
- Обновить `tests/controllers/v1/test_delivery.py` только для проверки API schema с `from_city`/`to_city` типа `int` и 422 на невалидные значения типа.
|
||||
- Обновить `tests/adapters/delivery_providers/cdek/test_client.py` для проверки построения payload по `cities_map`, ошибок при отсутствии `cdek.code` или city entry и отсутствия city lookup request.
|
||||
- При необходимости обновить `tests/smoke/test_app_import.py` только для совместимости нового request model с app wiring.
|
||||
|
||||
## Commands
|
||||
- `poetry run pytest tests/domain/test_price.py -q`
|
||||
- `poetry run pytest tests/services/test_aggregator.py -q`
|
||||
- `poetry run pytest tests/controllers/v1/test_delivery.py -q`
|
||||
- `poetry run pytest tests/adapters/delivery_providers/cdek/test_client.py -q`
|
||||
- `poetry run pytest tests/smoke/test_app_import.py -q`
|
||||
- `python3 spec/gen_spec_index.py --check`
|
||||
@@ -1,45 +0,0 @@
|
||||
---
|
||||
id: 021
|
||||
title: Add address suggestion adapter and country provider mapping
|
||||
status: DONE
|
||||
created: 2026-03-25
|
||||
---
|
||||
|
||||
## Context
|
||||
Перед созданием заказа клиенту нужно получить точное значение адреса для полей `from_location.address` и `to_location.address`. Для этого нужен отдельный address suggestion flow с внешним provider, а выбор provider должен определяться по `country_code` через YAML-конфиг.
|
||||
|
||||
## Goal
|
||||
Добавить contract для address suggestion adapters, секцию конфигурации address suggestions с маппингом `country_code -> provider_id` и реализовать интеграцию с `dadata.ru` для стран, сопоставленных с provider id `dadata`.
|
||||
|
||||
## Constraints
|
||||
- Изменения ограничены Adapter layer, config schema/loading и моделями, необходимыми для стабильного adapter contract.
|
||||
- Внешний IO должен оставаться внутри address suggestion adapter modules.
|
||||
- В scope задачи входит только интеграция с `dadata.ru`; concrete HTTP integration второго европейского provider не реализовывать в рамках этой задачи.
|
||||
- Конфигурация должна читаться из `.yaml` и содержать отдельную секцию для address suggestion providers и country mapping.
|
||||
- Adapter не должен содержать business decisions; он только отправляет запрос, маппит ответ и детерминированно обрабатывает provider/transport errors.
|
||||
- Не изменять файлы в `spec/`.
|
||||
- Поищи описание api dadata и следуй её описанию при составлении payload запроса
|
||||
|
||||
## Acceptance criteria
|
||||
- Существует интерфейс `AddressSuggestionProvider` со стабильным контрактом `suggest(request: AddressSuggestRequest) -> list[AddressSuggestion]`.
|
||||
- В YAML-конфиге добавлена секция address suggestions с настройками `dadata` и маппингом `country_code -> provider_id`.
|
||||
- Реализован adapter `dadata`, который принимает internal request model и возвращает унифицированный список `AddressSuggestion`.
|
||||
- Provider-specific поля ответа `dadata` не утекают за пределы adapter contract.
|
||||
- Ошибки `dadata` класса 4xx маппятся в детерминированную provider request error, а transport/5xx ошибки — в adapter client error.
|
||||
- Тесты покрывают загрузку конфигурации address suggestions, request/response mapping `dadata` и основные error scenarios.
|
||||
|
||||
## Definition of Done
|
||||
- [ ] Добавлен base contract для address suggestion providers.
|
||||
- [ ] Добавлена YAML-конфигурация address suggestions и country mapping.
|
||||
- [ ] Реализован adapter `dadata` для address suggestions.
|
||||
- [ ] Добавлены config и adapter tests для нового flow.
|
||||
|
||||
## Tests
|
||||
- Обновить `tests/config/test_config_sections.py` для проверки секции address suggestions и маппинга стран на provider id.
|
||||
- Добавить `tests/adapters/address_suggestions/dadata/test_client.py` для success/error сценариев и mapping.
|
||||
- Проверить, что наружу возвращается только унифицированная model `AddressSuggestion`.
|
||||
|
||||
## Commands
|
||||
- `poetry run pytest tests/config/test_config_sections.py -q`
|
||||
- `poetry run pytest tests/adapters/address_suggestions/dadata/test_client.py -q`
|
||||
- `python3 spec/gen_spec_index.py --check`
|
||||
@@ -1,49 +0,0 @@
|
||||
---
|
||||
id: 022
|
||||
title: Add address suggestion endpoint
|
||||
status: DONE
|
||||
created: 2026-03-25
|
||||
---
|
||||
|
||||
## Context
|
||||
Перед реализацией order flow клиенту нужен отдельный endpoint, который возвращает подсказки адреса и позволяет выбрать точное значение для `from_location.address` и `to_location.address`. Выбор provider должен происходить по `country_code` через конфигурационный маппинг стран.
|
||||
|
||||
## Goal
|
||||
Добавить `POST /api/v1/delivery/suggest-address` в существующий controller и существующий service с request/response schemas, routing на address suggestion provider по `country_code` и детерминированным HTTP error mapping.
|
||||
|
||||
## Constraints
|
||||
- Controller отвечает только за DTO validation, routing и mapping service exceptions в HTTP responses.
|
||||
- Endpoint должен вызывать ровно один метод Service: `AggregatorService.suggest_addresses()`.
|
||||
- Новый endpoint должен быть добавлен в существующий controller модуль `app/controllers/v1/delivery.py`; не создавать отдельный controller модуль.
|
||||
- Логика provider selection должна быть добавлена в существующий service модуль `app/services/aggregator.py`; не создавать отдельный service модуль.
|
||||
- Service выбирает provider по `country_code` через injected config mapping и вызывает ровно один address suggestion adapter; provider HTTP-детали в Service запрещены.
|
||||
- Scope задачи не включает создание заказа CDEK, изменение `POST /api/v1/delivery/order`, расчёт стоимости доставки, cache behavior и concrete HTTP integration европейского provider.
|
||||
- Не изменять файлы в `spec/`.
|
||||
|
||||
## Acceptance criteria
|
||||
- Существует endpoint `POST /api/v1/delivery/suggest-address`, принимающий `AddressSuggestRequest` и возвращающий `list[AddressSuggestion]`.
|
||||
- Реализованы request/response schemas `AddressSuggestRequest` и `AddressSuggestion`.
|
||||
- Endpoint реализован в существующем controller `app/controllers/v1/delivery.py`.
|
||||
- Controller делегирует обработку только в `AggregatorService.suggest_addresses()`.
|
||||
- Service определяет provider по `country_code` через конфигурационный маппинг и вызывает только соответствующий registered adapter.
|
||||
- Если `country_code` отсутствует в маппинге или сопоставлен с незарегистрированным provider, endpoint возвращает детерминированный 400 response.
|
||||
- Provider request errors маппятся в 400, недоступность внешнего сервиса и transport failures — в 503.
|
||||
- Service и controller/API tests покрывают как минимум сценарии: route в `dadata`, route в второй provider через test double, unsupported country и provider failure.
|
||||
|
||||
## Definition of Done
|
||||
- [ ] Добавлены address suggestion request/response schemas.
|
||||
- [ ] Реализован метод `AggregatorService.suggest_addresses()` для provider routing и orchestration.
|
||||
- [ ] Реализован endpoint `POST /api/v1/delivery/suggest-address` в существующем controller.
|
||||
- [ ] Добавлены service и controller/API tests для address suggestion flow.
|
||||
|
||||
## Tests
|
||||
- Добавить `tests/services/test_address_suggestions.py` для проверки routing по `country_code`, unsupported country, незарегистрированного provider и provider failures.
|
||||
- Добавить `tests/controllers/v1/test_address_suggestions.py` для success case, schema validation и HTTP mapping ошибок.
|
||||
- При необходимости обновить `tests/smoke/test_app_import.py` для проверки подключения нового endpoint и service wiring без новых controller/service модулей.
|
||||
- Использовать test doubles для address suggestion adapters.
|
||||
|
||||
## Commands
|
||||
- `poetry run pytest tests/services/test_address_suggestions.py -q`
|
||||
- `poetry run pytest tests/controllers/v1/test_address_suggestions.py -q`
|
||||
- `poetry run pytest tests/smoke/test_app_import.py -q`
|
||||
- `python3 spec/gen_spec_index.py --check`
|
||||
@@ -1,55 +0,0 @@
|
||||
---
|
||||
id: 023
|
||||
title: Add Yandex Geosuggest address suggestion adapter and CIS routing
|
||||
status: DONE
|
||||
created: 2026-03-29
|
||||
---
|
||||
|
||||
## Context
|
||||
Текущий flow подсказок адреса поддерживает только `dadata` и уже использует routing по `country_code` через YAML-конфиг. Появилось новое требование: для части стран СНГ использовать отдельный provider `Yandex Geosuggest`, чтобы разгрузить `dadata` и зафиксировать routing по странам на уровне конфигурации.
|
||||
|
||||
## Goal
|
||||
Добавить новый adapter `yandex_geosuggest` для address suggestions, настроить routing по `country_code` так, чтобы `RU`, `BY` и `KZ` оставались на `dadata`, а `AM`, `AZ`, `KG`, `MD`, `TJ`, `TM` и `UZ` шли через `Yandex Geosuggest`, и покрыть это тестами без изменения публичного API endpoint.
|
||||
|
||||
## Constraints
|
||||
- Scope задачи ограничен flow подсказок адреса: YAML-конфиг, wiring зависимостей, service routing, новый adapter и тесты.
|
||||
- Controller path и публичный контракт `POST /api/v1/delivery/suggest-address` не изменять.
|
||||
- Service остаётся orchestration layer: выбирает provider по injected config mapping и вызывает ровно один adapter без provider-specific HTTP-логики.
|
||||
- Внешний IO должен оставаться только внутри нового adapter `app/adapters/address_suggestions/yandex_geosuggest/`.
|
||||
- Для `RU`, `BY` и `KZ` existing mapping на `dadata` должен сохраниться без изменения контракта `dadata` adapter.
|
||||
- Для `AM`, `AZ`, `KG`, `MD`, `TJ`, `TM` и `UZ` конфиг должен выбирать provider id `yandex_geosuggest`.
|
||||
- Новый adapter должен следовать официальному HTTP-контракту Yandex Geosuggest: `GET https://suggest-maps.yandex.ru/v1/suggest`, обязательные query-параметры `apikey` и `text`; для полного адреса использовать `print_address=1`; если в internal request передан `limit`, он должен маппиться в query-параметр `results`.
|
||||
- Unified mapping наружу должен по-прежнему возвращать только `AddressSuggestion` без утечки provider-specific payload.
|
||||
- Для Yandex Geosuggest поле `postal_code` не извлекается из provider response и должно возвращаться как `None`.
|
||||
- Ошибки Yandex Geosuggest `400` должны маппиться в deterministic provider request error; `403`, `429`, transport errors и `5xx` должны маппиться в adapter client error/unavailable path.
|
||||
- Scope задачи не включает добавление европейского provider, изменение order flow, price flow, новых endpoint'ов и расширение internal model `AddressSuggestion`.
|
||||
- Не изменять файлы в `spec/`.
|
||||
|
||||
## Acceptance criteria
|
||||
- В конфиге address suggestion providers добавлен provider id `yandex_geosuggest` с параметрами, необходимыми для вызова Yandex Geosuggest.
|
||||
- Country mapping в конфиге маршрутизирует `RU`, `BY`, `KZ` на `dadata`, а `AM`, `AZ`, `KG`, `MD`, `TJ`, `TM`, `UZ` на `yandex_geosuggest`.
|
||||
- Реализован adapter `app/adapters/address_suggestions/yandex_geosuggest/client.py`, который отправляет запрос в Yandex Geosuggest по официальному контракту и возвращает `list[AddressSuggestion]`.
|
||||
- Adapter использует `request.city` и `request.query` как значение `text`, передаёт `print_address=1`, а `request.limit` при наличии маппит в `results`.
|
||||
- Поля `address`, `street`, `house` и `flat` детерминированно извлекаются из ответа Yandex Geosuggest без утечки внешнего payload за пределы adapter contract, а `postal_code` возвращается как `None`.
|
||||
- `AggregatorService.suggest_addresses()` по `country_code` выбирает `yandex_geosuggest` для стран `AM`, `AZ`, `KG`, `MD`, `TJ`, `TM`, `UZ` и сохраняет routing на `dadata` для `RU`, `BY`, `KZ`.
|
||||
- При ответе Yandex Geosuggest с `400` service flow возвращает deterministic bad-request path, а при `403`, `429`, `5xx` и transport failure — deterministic unavailable path.
|
||||
|
||||
## Definition of Done
|
||||
- [ ] Добавлена конфигурация `yandex_geosuggest` и обновлён mapping стран для address suggestions.
|
||||
- [ ] Реализован новый Yandex Geosuggest adapter без утечки HTTP-деталей в Service.
|
||||
- [ ] Обновлён wiring address suggestion providers без изменения публичного endpoint контракта.
|
||||
- [ ] Добавлены tests для config, adapter mapping/error handling и service routing по странам СНГ, включая `postal_code=None` для Yandex Geosuggest.
|
||||
- [ ] Пройдены все команды из раздела Commands.
|
||||
|
||||
## Tests
|
||||
- Обновить `tests/config/test_config_sections.py` для проверки секции `yandex_geosuggest` и явного country mapping: `RU`, `BY`, `KZ` -> `dadata`; `AM`, `AZ`, `KG`, `MD`, `TJ`, `TM`, `UZ` -> `yandex_geosuggest`.
|
||||
- Добавить `tests/adapters/address_suggestions/yandex_geosuggest/test_client.py` для success case, mapping `limit -> results`, извлечения `address`/`street`/`house`/`flat`, возврата `postal_code=None` и error scenarios `400`, `403`, `429`, `5xx`.
|
||||
- Обновить `tests/services/test_address_suggestions.py` для проверки routing на `yandex_geosuggest` по странам `AM`, `AZ`, `KG`, `MD`, `TJ`, `TM`, `UZ` и сохранения routing на `dadata` для `RU`, `BY`, `KZ`.
|
||||
- При необходимости обновить `tests/controllers/v1/test_address_suggestions.py` только для совместимости существующего endpoint с новым provider routing без изменения публичного API.
|
||||
|
||||
## Commands
|
||||
- `poetry run pytest tests/config/test_config_sections.py -q`
|
||||
- `poetry run pytest tests/adapters/address_suggestions/yandex_geosuggest/test_client.py -q`
|
||||
- `poetry run pytest tests/services/test_address_suggestions.py -q`
|
||||
- `poetry run pytest tests/controllers/v1/test_address_suggestions.py -q`
|
||||
- `python3 spec/gen_spec_index.py`
|
||||
@@ -1,56 +0,0 @@
|
||||
---
|
||||
id: 024
|
||||
title: Add TomTom address suggestion adapter and Europe routing
|
||||
status: DONE
|
||||
created: 2026-03-29
|
||||
---
|
||||
|
||||
## Context
|
||||
Текущий flow подсказок адреса поддерживает `dadata` для `RU`, `BY`, `KZ` и `yandex_geosuggest` для части стран СНГ. Следующий этап требует добавить третий provider `TomTom` для европейских стран, сохранив текущий публичный API и routing через YAML-конфиг.
|
||||
|
||||
## Goal
|
||||
Добавить новый adapter `tomtom` для address suggestions на базе TomTom Search API Fuzzy Search, настроить routing европейских стран на provider id `tomtom` через конфиг и покрыть это config, adapter и service tests без изменения публичного API endpoint.
|
||||
|
||||
## Constraints
|
||||
- Scope задачи ограничен flow подсказок адреса: YAML-конфиг, wiring зависимостей, service routing, новый adapter и тесты.
|
||||
- Controller path и публичный контракт `POST /api/v1/delivery/suggest-address` не изменять.
|
||||
- Service остаётся orchestration layer: выбирает provider по injected config mapping и вызывает ровно один adapter без provider-specific HTTP-логики.
|
||||
- Внешний IO должен оставаться только внутри нового adapter `app/adapters/address_suggestions/tomtom/`.
|
||||
- Новый adapter должен следовать официальному HTTP-контракту TomTom Search API Fuzzy Search: `GET https://api.tomtom.com/search/2/search/{query}.json`.
|
||||
- TomTom adapter должен формировать `query` из `request.city` и `request.query`, передавать `countrySet=request.country_code`, `typeahead=true`, а `request.limit` при наличии маппить в query-параметр `limit`.
|
||||
- Для исключения POI из address suggestion flow adapter должен отправлять address-oriented `idxSet=PAD,Addr,Str,EPP`.
|
||||
- Routing европейских стран должен определяться только YAML-маппингом `country_code -> provider_id`; запрещено добавлять в Service hardcoded классификацию Европы.
|
||||
- Unified mapping наружу должен возвращать только `AddressSuggestion` без утечки provider-specific payload.
|
||||
- Для TomTom Search поля должны маппиться детерминированно: `address.freeformAddress -> address`, `address.streetName -> street`, `address.streetNumber -> house`, `address.postalCode -> postal_code`, `flat=None`.
|
||||
- Ошибки TomTom `400` должны маппиться в deterministic provider request error; `403`, `429`, transport errors и `5xx` должны маппиться в adapter client error/unavailable path.
|
||||
- Scope задачи не включает изменение order flow, price flow, новых endpoint'ов, расширение internal model `AddressSuggestion` и изменение контрактов существующих adapters `dadata` и `yandex_geosuggest`.
|
||||
- Не изменять файлы в `spec/`.
|
||||
|
||||
## Acceptance criteria
|
||||
- В конфиге address suggestion providers добавлен provider id `tomtom` с параметрами, необходимыми для вызова TomTom Search API Fuzzy Search.
|
||||
- Country mapping в конфиге маршрутизирует на `tomtom` европейские страны, явно перечисленные в YAML, при этом routing для `RU`, `BY`, `KZ` на `dadata` и для `AM`, `AZ`, `KG`, `MD`, `TJ`, `TM`, `UZ` на `yandex_geosuggest` сохраняется без изменений.
|
||||
- Реализован adapter `app/adapters/address_suggestions/tomtom/client.py`, который отправляет запрос в TomTom Search API Fuzzy Search по официальному контракту и возвращает `list[AddressSuggestion]`.
|
||||
- Adapter объединяет `request.city` и `request.query` в search query, передаёт `countrySet=request.country_code`, `typeahead=true`, `idxSet=PAD,Addr,Str,EPP` и при наличии `request.limit` маппит его в `limit`.
|
||||
- Поля `address`, `street`, `house` и `postal_code` детерминированно извлекаются из ответа TomTom (`freeformAddress`, `streetName`, `streetNumber`, `postalCode`), а `flat` возвращается как `None`.
|
||||
- `AggregatorService.suggest_addresses()` по `country_code` выбирает `tomtom` для европейских стран, явно сопоставленных в YAML-конфиге, и сохраняет существующий routing на `dadata` и `yandex_geosuggest` для уже поддержанных стран.
|
||||
- При ответе TomTom с `400` service flow возвращает deterministic bad-request path, а при `403`, `429`, `5xx` и transport failure — deterministic unavailable path.
|
||||
|
||||
## Definition of Done
|
||||
- [ ] Добавлена конфигурация `tomtom` и обновлён country mapping для европейских стран.
|
||||
- [ ] Реализован новый TomTom adapter без утечки HTTP-деталей в Service.
|
||||
- [ ] Обновлён wiring address suggestion providers без изменения публичного endpoint контракта.
|
||||
- [ ] Добавлены tests для config, adapter mapping/error handling и service routing на `tomtom` с сохранением существующего routing на `dadata` и `yandex_geosuggest`.
|
||||
- [ ] Пройдены все команды из раздела Commands.
|
||||
|
||||
## Tests
|
||||
- Обновить `tests/config/test_config_sections.py` для проверки секции `tomtom` и country mapping, в котором европейские страны маршрутизируются на `tomtom`, а существующие маппинги `RU`, `BY`, `KZ` -> `dadata` и `AM`, `AZ`, `KG`, `MD`, `TJ`, `TM`, `UZ` -> `yandex_geosuggest` сохраняются.
|
||||
- Добавить `tests/adapters/address_suggestions/tomtom/test_client.py` для success case, mapping `limit`, формирования search query из `city` и `query`, передачи `typeahead=true` и `idxSet=PAD,Addr,Str,EPP`, возврата `flat=None`, извлечения `address`/`street`/`house`/`postal_code` и error scenarios `400`, `403`, `429`, `5xx`.
|
||||
- Обновить `tests/services/test_address_suggestions.py` для проверки routing на `tomtom` по европейским странам из YAML-конфига и сохранения routing на `dadata` и `yandex_geosuggest` для уже поддержанных стран.
|
||||
- При необходимости обновить `tests/controllers/v1/test_address_suggestions.py` только для совместимости существующего endpoint с новым provider routing без изменения публичного API.
|
||||
|
||||
## Commands
|
||||
- `poetry run pytest tests/config/test_config_sections.py -q`
|
||||
- `poetry run pytest tests/adapters/address_suggestions/tomtom/test_client.py -q`
|
||||
- `poetry run pytest tests/services/test_address_suggestions.py -q`
|
||||
- `poetry run pytest tests/controllers/v1/test_address_suggestions.py -q`
|
||||
- `python3 spec/gen_spec_index.py --check`
|
||||
@@ -1,51 +0,0 @@
|
||||
---
|
||||
id: 025
|
||||
title: Remove company from Create Delivery Order parties
|
||||
status: DONE
|
||||
created: 2026-04-03
|
||||
---
|
||||
|
||||
## Context
|
||||
Текущий контракт `POST /api/v1/delivery/order` допускает поле `company` в `sender`, потому что order party schema используется и для `sender`, и для `recipient`. Новый сценарий требует упростить Create Delivery Order payload: поля `sender.company` и `recipient.company` больше не должны приниматься и не должны отправляться в CDEK.
|
||||
|
||||
## Goal
|
||||
Удалить `company` из request contract Create Delivery Order на уровне schema validation, controller/API contract и CDEK order registration payload без изменения endpoint path, service orchestration и response model.
|
||||
|
||||
## Constraints
|
||||
- Изменения ограничены существующими модулями order flow: `app/schemas/order.py`, `app/controllers/v1/delivery.py`, `app/services/aggregator.py`, `app/adapters/delivery_providers/cdek/` и связанными тестами; не создавать новые controller/service модули.
|
||||
- `POST /api/v1/delivery/order` должен оставаться в существующем controller и по-прежнему вызывать ровно один метод Service: `AggregatorService.create_order()`.
|
||||
- Service не должен получать новую business logic; orchestration order flow должна остаться прежней.
|
||||
- Поля `sender.company` и `recipient.company` должны отсутствовать в публичном request contract и не должны сериализоваться в payload, отправляемый в CDEK.
|
||||
- Payload, содержащий `sender.company` или `recipient.company`, должен считаться невалидным и отклоняться schema validation с HTTP 422.
|
||||
- `from_location.address` и `to_location.address` остаются opaque input strings; не добавлять address suggestion lookup, нормализацию адреса или иные изменения order flow.
|
||||
- Scope задачи не включает изменение response contract, price flow, address suggestion flow, provider routing, кеширование, новые провайдеры и расширение поддерживаемых `type`/`tariff_code`.
|
||||
- Обновить пример контракта Create Delivery Order в `http-client.http`, чтобы он не содержал `company`.
|
||||
- Не изменять файлы в `spec/`.
|
||||
|
||||
## Acceptance criteria
|
||||
- `OrderCreateRequest` больше не содержит поле `company` ни для `sender`, ни для `recipient`.
|
||||
- `POST /api/v1/delivery/order` успешно принимает прежний payload без `company` и не требует никаких новых полей.
|
||||
- Если request payload содержит `sender.company` или `recipient.company`, endpoint возвращает 422 на уровне schema validation.
|
||||
- `AggregatorService.create_order()` продолжает только оркестрировать вызов injected order adapter без новой business logic.
|
||||
- CDEK adapter отправляет JSON payload для регистрации заказа без поля `company`.
|
||||
- `http-client.http` содержит актуальный пример регистрации заказа без `company`.
|
||||
- Service, controller/API и adapter tests покрывают success case после удаления поля и rejection scenario для `company`.
|
||||
|
||||
## Definition of Done
|
||||
- [ ] Обновлён order request contract без поля `company` для `sender` и `recipient`.
|
||||
- [ ] Schema validation отклоняет `sender.company` и `recipient.company`.
|
||||
- [ ] CDEK order registration payload больше не содержит `company`.
|
||||
- [ ] Обновлены order flow tests для service, controller/API и adapter layers.
|
||||
- [ ] Обновлён пример Create Delivery Order в `http-client.http`.
|
||||
|
||||
## Tests
|
||||
- Обновить `tests/services/test_order.py`, убрав `company` из валидных fixture payloads и сохранив проверку service orchestration.
|
||||
- Обновить `tests/controllers/v1/test_order.py` для success case без `company` и добавить сценарий 422 при передаче `sender.company` и `recipient.company`.
|
||||
- Обновить `tests/adapters/delivery_providers/cdek/test_order_client.py`, чтобы проверить отсутствие `company` в отправляемом CDEK payload.
|
||||
- При необходимости обновить другие order-related tests, завязанные на старый request contract.
|
||||
|
||||
## Commands
|
||||
- `poetry run pytest tests/services/test_order.py -q`
|
||||
- `poetry run pytest tests/controllers/v1/test_order.py -q`
|
||||
- `poetry run pytest tests/adapters/delivery_providers/cdek/test_order_client.py -q`
|
||||
- `python3 spec/gen_spec_index.py --check`
|
||||
@@ -1,52 +0,0 @@
|
||||
---
|
||||
id: 026
|
||||
title: Align CDEK order contract with single phone and kilogram package weight
|
||||
status: DONE
|
||||
created: 2026-04-05
|
||||
---
|
||||
|
||||
## Context
|
||||
Текущий order flow принимает `sender.phones` и `recipient.phones` как список, требует обязательное поле `services` и пробрасывает `packages.weight` в CDEK payload без явной фиксации единиц измерения. Новый контракт должен принимать один телефон на сторону, разрешать отсутствие `services` и гарантировать, что во входном API вес упаковки задаётся в килограммах, а в CDEK отправляется в граммах.
|
||||
|
||||
## Goal
|
||||
Обновить flow `POST /api/v1/delivery/order` по слоям Controller, Service и Adapter так, чтобы public/internal request contract использовал `phone` вместо `phones`, поле `services` было необязательным, а CDEK order mapper конвертировал `packages.weight` из килограммов во входном запросе в граммы во внешнем provider payload.
|
||||
|
||||
## Constraints
|
||||
- Изменения ограничены существующими модулями order flow: `app/schemas/order.py`, `app/controllers/v1/delivery.py`, `app/services/aggregator.py`, `app/adapters/delivery_providers/cdek/`, `http-client.http` и связанными тестами.
|
||||
- `POST /api/v1/delivery/order` должен оставаться в существующем controller и по-прежнему вызывать ровно один метод Service: `AggregatorService.create_order()`.
|
||||
- Service остаётся orchestration layer и не получает новую business logic; он только принимает обновлённую order model и делегирует её adapter.
|
||||
- Во внутреннем order flow и публичном API поле `phones` должно быть удалено; передача нескольких телефонов больше не поддерживается.
|
||||
- CDEK adapter может сериализовать внутренний `phone` в provider-specific поле `phones`, если это требуется внешним контрактом CDEK, но множественность телефонов не должна возвращаться во внутренние модели и controller contract.
|
||||
- `services` должно быть необязательным полем request schema; при отсутствии значения нельзя подставлять фиктивные service entries.
|
||||
- Конвертация единиц `packages.weight` должна выполняться только на границе CDEK adapter mapping: входной order request использует килограммы, исходящий payload в CDEK использует граммы.
|
||||
- Scope задачи не включает изменение response contract, price flow, address suggestion flow, provider routing, кеширование, новые провайдеры и расширение поддерживаемых `type`/`tariff_code`.
|
||||
- Не изменять файлы в `spec/`.
|
||||
|
||||
## Acceptance criteria
|
||||
- `OrderCreateRequest` использует `sender.phone` и `recipient.phone` вместо `sender.phones` и `recipient.phones`.
|
||||
- Если request payload содержит `sender.phones` или `recipient.phones`, endpoint возвращает 422 на уровне schema validation.
|
||||
- `POST /api/v1/delivery/order` принимает валидный payload без поля `services`.
|
||||
- Если `services` отсутствует или равен `null`, service и adapter flow успешно обрабатывают запрос без добавления `services` в исходящий CDEK payload.
|
||||
- `AggregatorService.create_order()` продолжает только оркестрировать вызов injected order adapter и не содержит преобразования `phone`/`phones` или килограммов в граммы.
|
||||
- CDEK order mapper формирует provider payload с полями `sender.phones` и `recipient.phones`, каждое из которых содержит ровно один элемент, полученный из соответствующего внутреннего поля `phone`.
|
||||
- Для каждого элемента `packages` исходящий payload CDEK содержит `weight`, равный значению входного `packages.weight`, умноженному на `1000`.
|
||||
- `http-client.http` содержит актуальный пример Create Delivery Order с `phone` вместо `phones`, без обязательного `services` и с весом, отражающим controller contract в килограммах.
|
||||
|
||||
## Definition of Done
|
||||
- [ ] Обновлён public/internal order request contract на `phone` вместо `phones`.
|
||||
- [ ] `services` сделано необязательным без изменения endpoint path и service orchestration.
|
||||
- [ ] Реализован mapping одного телефона в provider payload `phones` и конвертация `packages.weight` из килограммов в граммы.
|
||||
- [ ] Обновлены controller, service и adapter tests под новый контракт и unit conversion.
|
||||
- [ ] Обновлён пример запроса в `http-client.http`.
|
||||
|
||||
## Tests
|
||||
- Обновить `tests/controllers/v1/test_order.py` для success case с `phone`, сценариев 422 при передаче `sender.phones` и `recipient.phones`, а также для запроса без `services`.
|
||||
- Обновить `tests/services/test_order.py` для проверки, что service принимает обновлённую order model, делегирует её adapter без новой логики и корректно работает с `services=None`.
|
||||
- Обновить `tests/adapters/delivery_providers/cdek/test_order_client.py` для проверки mapping `phone -> phones[0]`, отсутствия `services` в исходящем payload при `None` и конвертации `packages.weight` из килограммов в граммы.
|
||||
- При необходимости обновить другие order-related tests и fixtures, завязанные на старые поля `phones` и обязательность `services`.
|
||||
|
||||
## Commands
|
||||
- `poetry run pytest tests/controllers/v1/test_order.py -q`
|
||||
- `poetry run pytest tests/services/test_order.py -q`
|
||||
- `poetry run pytest tests/adapters/delivery_providers/cdek/test_order_client.py -q`
|
||||
- `python3 spec/gen_spec_index.py --check`
|
||||
@@ -1,72 +0,0 @@
|
||||
---
|
||||
id: 027
|
||||
title: Add TBank payment adapter, init_payment endpoint and rename order flow
|
||||
status: DONE
|
||||
created: 2026-04-11
|
||||
---
|
||||
|
||||
## Context
|
||||
В проекте ещё нет платёжного адаптера, каталог `app/adapters/` содержит только `delivery_providers/` и `address_suggestions/`. Текущий endpoint `POST /api/v1/delivery/order` регистрирует заказ в CDEK. В новом флоу endpoint будет инициировать оплату через TBank и возвращать ссылку на оплату, без регистрации заказа в CDEK. В связи с изменением бизнес-смысла необходимо переименовать endpoint, сервисный метод, все DTO и модели флоу.
|
||||
|
||||
## Goal
|
||||
1. Добавить новый payment adapter `TBankAdapter` для генерации ссылки на оплату TBank.
|
||||
2. Переименовать endpoint `POST /api/v1/delivery/order` → `POST /api/v1/delivery/init-payment`.
|
||||
3. Переименовать сервисный метод `AggregatorService.create_order()` → `AggregatorService.init_payment()`.
|
||||
4. Переименовать все DTO и модели флоу: `OrderCreateRequest` → `InitPaymentRequest`, `OrderCreateResponse` → `InitPaymentResponse`, `InvalidOrderCreateRequestError` → `InvalidInitPaymentRequestError`, `OrderCreationUnavailableError` → `InitPaymentUnavailableError`.
|
||||
5. Переименовать файл схем `app/schemas/order.py` → `app/schemas/payment.py`.
|
||||
6. Добавить в `InitPaymentRequest` обязательное поле `price: int` (в копейках) и вызвать `payment_adapter.create_payment_link()` из `init_payment()`, вернув `payment_url` в `InitPaymentResponse`.
|
||||
|
||||
## Constraints
|
||||
- Новый payment adapter должен располагаться в отдельном каталоге `app/adapters/tbank/` с модулями `base.py` и `client.py`; не размещать payment logic внутри `delivery_providers/` или `address_suggestions/`.
|
||||
- Payment adapter MUST инкапсулировать HTTP взаимодействие с TBank API, auth, retries, serialization и error handling; наружу должен экспонироваться только service-facing method (`create_payment_link(order_uuid: str, amount_kopecks: int) -> str`), без утечки HTTP деталей в Service.
|
||||
- Payment adapter MUST владеть собственной секцией конфигурации (`TBankPaymentConfig`) с обязательными полями auth и URL; конфигурация читается из `config.yaml`, который остаётся в `.gitignore`.
|
||||
- `InitPaymentRequest` MUST содержать обязательное поле `price: int` в копейках с валидацией `gt=0`; единица измерения явно зафиксирована в schema и в `spec/overview.md`.
|
||||
- `AggregatorService.init_payment()` MUST вызывать `payment_adapter.create_payment_link()`. Payment adapter передаётся в service через dependency injection (новый аргумент конструктора).
|
||||
- Ошибки payment adapter MUST маппиться в `InvalidInitPaymentRequestError` (→ 400) и `InitPaymentUnavailableError` (→ 503) на уровне controller.
|
||||
- Controller `POST /api/v1/delivery/init-payment` MUST вызывать ровно один метод Service (`AggregatorService.init_payment()`); старый endpoint `/order` удаляется, новые дополнительные endpoints запрещены.
|
||||
- Service слой не содержит HTTP, retries или TBank-специфичных деталей.
|
||||
- Scope задачи не включает: повторные попытки оплаты, refund flow, webhook обработку платёжных уведомлений, persistence заказов, изменения price flow, address suggestion flow, а также поддержку других платёжных провайдеров.
|
||||
- Не изменять файлы в `spec/` кроме создания этой задачи и обновления `spec/overview.md` при необходимости (обновление выполняется Planner).
|
||||
|
||||
## Acceptance criteria
|
||||
- В `app/adapters/tbank/` существуют `base.py` с исключениями адаптера и `client.py` с реализацией `TBankAdapter`.
|
||||
- `TBankAdapter` реализует async метод `create_payment_link(order_uuid: str, amount_kopecks: int) -> str`, принимает идентификатор заказа и сумму в копейках и возвращает URL.
|
||||
- `TBankAdapter` имеет собственную конфигурацию `TBankPaymentConfig` с обязательными полями auth и URL; конфигурация читается из yaml.
|
||||
- Файл `app/schemas/order.py` переименован в `app/schemas/payment.py`; все импорты обновлены.
|
||||
- `InitPaymentRequest` (бывший `OrderCreateRequest`) содержит обязательное поле `price: int` (в копейках) с валидацией `gt=0`; запрос без `price` или с нецелым/отрицательным значением возвращает 422.
|
||||
- `InitPaymentResponse` (бывший `OrderCreateResponse`) содержит поле `payment_url: str` (минимум 1 символ) и возвращается из endpoint `POST /api/v1/delivery/init-payment`.
|
||||
- `AggregatorService.init_payment()` (бывший `create_order()`) принимает `InitPaymentRequest`, вызывает `payment_adapter.create_payment_link()` с `order_uuid` и `price`, и возвращает `InitPaymentResponse` с `payment_url`.
|
||||
- Сервисные исключения переименованы: `InvalidInitPaymentRequestError` (бывший `InvalidOrderCreateRequestError`), `InitPaymentUnavailableError` (бывший `OrderCreationUnavailableError`).
|
||||
- Endpoint `POST /api/v1/delivery/order` удалён; endpoint `POST /api/v1/delivery/init-payment` возвращает `InitPaymentResponse`.
|
||||
- Ошибки TBank provider request мапятся в 400, transport/недоступность — в 503 на уровне controller.
|
||||
- Controller продолжает вызывать ровно один метод Service.
|
||||
- Пример запроса в `http-client.http` обновлён: использует `/init-payment`, содержит поле `price` в копейках и отражает обновлённый response contract.
|
||||
- `config.yaml` пример (в `.gitignore`) содержит секцию с параметрами TBank adapter.
|
||||
|
||||
## Definition of Done
|
||||
- [ ] Создан модуль `app/adapters/tbank/` с `base.py` и `client.py`, реализующий `TBankAdapter`.
|
||||
- [ ] Добавлена конфигурация TBank adapter (`TBankPaymentConfig`) в `app/config.py` и соответствующий пример в `config.yaml`.
|
||||
- [ ] `app/schemas/order.py` переименован в `app/schemas/payment.py`; все импорты в контроллере, сервисе и тестах обновлены.
|
||||
- [ ] `OrderCreateRequest` → `InitPaymentRequest`, `OrderCreateResponse` → `InitPaymentResponse` во всех файлах.
|
||||
- [ ] `InvalidOrderCreateRequestError` → `InvalidInitPaymentRequestError`, `OrderCreationUnavailableError` → `InitPaymentUnavailableError` во всех файлах.
|
||||
- [ ] `AggregatorService.create_order()` → `AggregatorService.init_payment()`; метод вызывает TBank adapter и возвращает `InitPaymentResponse`.
|
||||
- [ ] Payment adapter injected в service через wiring в `app/controllers/v1/delivery.py::_build_aggregator_service`.
|
||||
- [ ] Endpoint `/order` удалён, добавлен `/init-payment`; controller function переименована в `init_payment`.
|
||||
- [ ] Controller маппит `InvalidInitPaymentRequestError` → 400, `InitPaymentUnavailableError` → 503.
|
||||
- [ ] Обновлены unit tests для schemas, service, controller, а также добавлены unit tests для TBank adapter (HTTP client с stub transport).
|
||||
- [ ] Обновлён пример запроса в `http-client.http`.
|
||||
|
||||
## Tests
|
||||
- Добавить `tests/adapters/tbank/test_client.py` с проверкой: формирование payment request payload (auth, сумма в копейках, order uuid), маппинг успешного ответа в URL, маппинг 4xx в provider request error, маппинг 5xx/transport в client error, retry behavior если реализован.
|
||||
- Переименовать `tests/services/test_order.py` → `tests/services/test_init_payment.py`; обновить: stub payment adapter; success case включает вызов payment adapter и возврат `payment_url`; маппинг payment adapter errors в `InvalidInitPaymentRequestError` / `InitPaymentUnavailableError`.
|
||||
- Переименовать `tests/controllers/v1/test_order.py` → `tests/controllers/v1/test_init_payment.py`; обновить: endpoint `/init-payment`; payload содержит `price`; success response включает `payment_url`; запрос без `price` и с `price <= 0` возвращает 422; сценарии 400/503 продолжают работать.
|
||||
- Обновить `tests/config/test_config_sections.py` если добавлена новая обязательная секция в yaml.
|
||||
- При необходимости обновить `tests/smoke/test_app_import.py` для проверки wiring payment adapter.
|
||||
|
||||
## Commands
|
||||
- `poetry run pytest tests/adapters/tbank/test_client.py -q`
|
||||
- `poetry run pytest tests/services/test_init_payment.py -q`
|
||||
- `poetry run pytest tests/controllers/v1/test_init_payment.py -q`
|
||||
- `poetry run pytest tests/config/test_config_sections.py -q`
|
||||
- `poetry run pytest tests/smoke/test_app_import.py -q`
|
||||
- `python3 spec/gen_spec_index.py --check`
|
||||
@@ -1,75 +0,0 @@
|
||||
---
|
||||
id: 028
|
||||
title: Add PostgreSQL adapter, order repository and persist order after payment link creation
|
||||
status: DONE
|
||||
created: 2026-04-12
|
||||
---
|
||||
|
||||
## Context
|
||||
После создания ссылки на оплату через TBank adapter данные заявки нигде не сохраняются. Необходимо добавить PostgreSQL-адаптер для управления подключением к базе данных, репозиторий для сохранения данных заявки и интегрировать сохранение в существующий flow `AggregatorService.init_payment()`. Зависимости `sqlalchemy` (2.0.49) и `asyncpg` (0.31.0) уже присутствуют в `pyproject.toml`. Alembic (1.18.4) также доступен для миграций.
|
||||
|
||||
## Goal
|
||||
1. Добавить PostgreSQL-адаптер (`app/adapters/postgres/`) для управления async-сессиями SQLAlchemy (`AsyncEngine`, `async_sessionmaker`).
|
||||
2. Добавить репозиторий заявок (`app/repositories/order/`) с методом `create_order()` для сохранения данных заявки вместе со ссылкой на оплату в PostgreSQL.
|
||||
3. Определить SQLAlchemy model для таблицы заявок в `app/repositories/order/models.py`.
|
||||
4. Создать Alembic-миграцию для создания таблицы заявок.
|
||||
5. Расширить `AggregatorService.init_payment()`: после успешного получения `payment_url` от TBank adapter сохранять данные `InitPaymentRequest` вместе с `payment_url` через order repository.
|
||||
6. Добавить секцию конфигурации PostgreSQL (`PostgresConfig`) в `app/config.py` и пример в `config.yaml`.
|
||||
7. Добавить сервис PostgreSQL в `docker-compose.yml`.
|
||||
|
||||
## Constraints
|
||||
- PostgreSQL adapter (`app/adapters/postgres/`) MUST содержать только управление подключением (engine, session factory). Без бизнес-логики, без SQL-запросов.
|
||||
- Repository (`app/repositories/order/`) MUST содержать только операции с базой данных. Без бизнес-решений, без workflow-логики.
|
||||
- Service MUST оркестрировать вызовы TBank adapter и order repository. Если сохранение в БД завершается ошибкой после успешного получения `payment_url`, Service MUST всё равно вернуть `payment_url` клиенту (сохранение не должно блокировать ответ); ошибку сохранения логировать.
|
||||
- SQLAlchemy model MUST использовать `sqlalchemy.orm.DeclarativeBase` (SQLAlchemy 2.0 style).
|
||||
- Для миграций использовать Alembic с async-конфигурацией (`asyncpg`).
|
||||
- Конфигурация PostgreSQL MUST быть в отдельной секции `postgres` в `config.yaml` с обязательным полем `dsn`; `config.yaml` уже в `.gitignore`.
|
||||
- `_RequiredYamlSections` в `app/config.py` MUST быть обновлён для включения секции `postgres`.
|
||||
- Order repository передаётся в `AggregatorService` через dependency injection (новый параметр конструктора).
|
||||
- PostgreSQL adapter создаётся в wiring (`_build_aggregator_service`) в controller и передаёт session factory в order repository.
|
||||
- Таблица заявок MUST содержать как минимум: `id` (UUID, PK), `order_uuid` (str, unique), `payment_url` (str), `price` (int, копейки), `tariff_code` (int), `sender` (JSONB), `recipient` (JSONB), `from_location` (JSONB), `to_location` (JSONB), `packages` (JSONB), `services` (JSONB, nullable), `comment` (str, nullable), `created_at` (timestamp with timezone, server default).
|
||||
- Scope НЕ включает: чтение/обновление/удаление заявок, API-endpoint для списка заявок, webhook-обработку платёжных уведомлений, изменения price flow, address suggestion flow.
|
||||
- НЕ изменять существующие тесты TBank adapter, не изменять поведение price и address suggestion endpoints.
|
||||
|
||||
## Acceptance criteria
|
||||
- В `app/adapters/postgres/` существует модуль с функцией создания `AsyncEngine` и `async_sessionmaker` из конфигурации.
|
||||
- В `app/repositories/order/` существует `OrderRepository` с async-методом `create_order(session, order_data)`, сохраняющим запись заявки.
|
||||
- В `app/repositories/order/models.py` определена SQLAlchemy ORM model таблицы `orders` со всеми обязательными полями.
|
||||
- Alembic инициализирован с async-конфигурацией; существует миграция для создания таблицы `orders`.
|
||||
- `AggregatorService.__init__()` принимает опциональный `order_repository` через DI.
|
||||
- `AggregatorService.init_payment()` после успешного получения `payment_url` вызывает `order_repository.create_order()` с данными из `InitPaymentRequest` и `payment_url`.
|
||||
- Если `order_repository.create_order()` выбрасывает исключение, `init_payment()` логирует ошибку и возвращает `InitPaymentResponse(payment_url=...)` без ошибки клиенту.
|
||||
- В `app/config.py` добавлена `PostgresConfig` с полем `dsn: str`.
|
||||
- Секция `postgres` присутствует в `_RequiredYamlSections`.
|
||||
- В `docker-compose.yml` добавлен сервис `postgres` и `app` зависит от него.
|
||||
- Wiring в `_build_aggregator_service` создаёт PostgreSQL engine, session factory, `OrderRepository` и передаёт его в `AggregatorService`.
|
||||
- Запросы к эндпоинту `POST /api/v1/delivery/order` продолжают возвращать `InitPaymentResponse` с `payment_url`.
|
||||
|
||||
## Definition of Done
|
||||
- [ ] Создан модуль `app/adapters/postgres/` с engine/session factory.
|
||||
- [ ] Создан `app/repositories/order/repository.py` с `OrderRepository.create_order()`.
|
||||
- [ ] Создан `app/repositories/order/models.py` с ORM model таблицы `orders`.
|
||||
- [ ] Alembic инициализирован (`alembic.ini`, `alembic/`), создана миграция для таблицы `orders`.
|
||||
- [ ] Добавлена `PostgresConfig` в `app/config.py`; `_RequiredYamlSections` обновлён.
|
||||
- [ ] `AggregatorService` принимает `order_repository` через DI и использует его в `init_payment()`.
|
||||
- [ ] Ошибки сохранения заявки не блокируют возврат `payment_url` клиенту.
|
||||
- [ ] Обновлён wiring в `app/controllers/v1/delivery.py`.
|
||||
- [ ] Добавлен сервис `postgres` в `docker-compose.yml`.
|
||||
- [ ] `config.yaml` пример содержит секцию `postgres`.
|
||||
- [ ] `config.test.yaml` содержит секцию `postgres` (может использовать sqlite или тестовый DSN).
|
||||
- [ ] Все существующие тесты продолжают проходить.
|
||||
- [ ] Добавлены новые тесты.
|
||||
|
||||
## Tests
|
||||
- Добавить `tests/repositories/order/test_repository.py`: проверка `create_order()` с in-memory SQLite async engine (SQLAlchemy async); проверка, что все обязательные поля сохраняются; проверка обработки дублирования `order_uuid` (unique constraint).
|
||||
- Обновить `tests/services/test_init_payment.py`: добавить test case, где `order_repository.create_order()` вызывается после успешного создания payment link; добавить test case, где `order_repository.create_order()` выбрасывает исключение, а `init_payment()` всё равно возвращает `payment_url`.
|
||||
- Обновить `tests/config/test_config_sections.py` для проверки наличия секции `postgres` в yaml.
|
||||
- При необходимости обновить `tests/smoke/test_app_import.py` для проверки wiring order repository.
|
||||
|
||||
## Commands
|
||||
- `poetry run pytest tests/repositories/order/test_repository.py -q`
|
||||
- `poetry run pytest tests/services/test_init_payment.py -q`
|
||||
- `poetry run pytest tests/config/test_config_sections.py -q`
|
||||
- `poetry run pytest tests/smoke/test_app_import.py -q`
|
||||
- `poetry run pytest -q`
|
||||
- `python3 spec/gen_spec_index.py --check`
|
||||
@@ -1,54 +0,0 @@
|
||||
---
|
||||
id: 029
|
||||
title: Add TBank payment notification and success URLs
|
||||
status: DONE
|
||||
created: 2026-04-18
|
||||
---
|
||||
|
||||
## Context
|
||||
TBank Init API должен получать URLs для обработки webhook-уведомлений и возврата клиента после успешной оплаты. Сейчас `TBankAdapter` и секция `tbank_payment` не фиксируют передачу `NotificationURL` и `SuccessURL`.
|
||||
|
||||
## Goal
|
||||
Добавить в конфигурацию TBank payment обязательные параметры `notification_url` и `success_url` и передавать их в payload инициализации платежа как `NotificationURL` и `SuccessURL`.
|
||||
|
||||
## Constraints
|
||||
- Изменения ограничены TBank payment config, TBank adapter payload mapping, wiring конфигурации и тестами.
|
||||
- `NotificationURL` и `SuccessURL` MUST приходить из секции `tbank_payment` YAML-конфига.
|
||||
- TBank adapter MUST продолжать инкапсулировать HTTP-взаимодействие, auth token, retries, timeout, serialization и error handling.
|
||||
- Service и Controller MUST NOT содержать TBank-specific payload fields или provider HTTP-детали.
|
||||
- Не изменять публичный request/response contract `POST /api/v1/delivery/order`.
|
||||
- Не изменять price flow, address suggestion flow, CDEK adapter и order repository.
|
||||
- Не добавлять новые endpoints.
|
||||
|
||||
## Acceptance criteria
|
||||
- `TBankPaymentConfig` содержит обязательные поля `notification_url: str` и `success_url: str`.
|
||||
- Runtime YAML-конфиг и test YAML-конфиг содержат секцию `tbank_payment` с `notification_url` и `success_url`.
|
||||
- `TBankAdapter` при вызове TBank Init API отправляет `NotificationURL` со значением `tbank_payment.notification_url`.
|
||||
- `TBankAdapter` при вызове TBank Init API отправляет `SuccessURL` со значением `tbank_payment.success_url`.
|
||||
- Auth token TBank формируется с учётом тех же полей payload, которые отправляются в Init API, включая `NotificationURL` и `SuccessURL`, если текущая реализация token generation строится по request payload.
|
||||
- Отсутствие `notification_url` или `success_url` в YAML-конфиге приводит к детерминированной ошибке валидации конфигурации.
|
||||
- `AggregatorService.init_payment()` продолжает вызывать `payment_adapter.create_payment_link(order_uuid, price)` без дополнительных URL-аргументов.
|
||||
- Endpoint `POST /api/v1/delivery/order` продолжает возвращать только `InitPaymentResponse(payment_url=...)`.
|
||||
|
||||
## Definition of Done
|
||||
- [ ] В `app/config.py` добавлены поля `notification_url` и `success_url` в TBank payment config.
|
||||
- [ ] YAML-конфиги обновлены новыми параметрами TBank payment.
|
||||
- [ ] `TBankAdapter` передаёт `NotificationURL` и `SuccessURL` в payload TBank Init API.
|
||||
- [ ] Token generation для TBank request остаётся корректной после добавления новых payload fields.
|
||||
- [ ] Service и Controller не знают о `NotificationURL` и `SuccessURL`.
|
||||
- [ ] Добавлены или обновлены тесты конфигурации.
|
||||
- [ ] Добавлены или обновлены тесты TBank adapter payload.
|
||||
- [ ] Все команды из раздела Commands проходят.
|
||||
|
||||
## Tests
|
||||
- Обновить `tests/config/test_config_sections.py`: проверить обязательность `tbank_payment.notification_url` и `tbank_payment.success_url`.
|
||||
- Обновить `tests/adapters/tbank/test_client.py`: проверить, что successful Init request payload содержит `NotificationURL` и `SuccessURL` из конфигурации.
|
||||
- Обновить `tests/adapters/tbank/test_client.py`: проверить, что token generation остаётся детерминированной после добавления новых payload fields.
|
||||
- При необходимости обновить `tests/smoke/test_app_import.py`, если smoke config требует новые обязательные поля.
|
||||
|
||||
## Commands
|
||||
- `poetry run pytest tests/config/test_config_sections.py -q`
|
||||
- `poetry run pytest tests/adapters/tbank/test_client.py -q`
|
||||
- `poetry run pytest tests/smoke/test_app_import.py -q`
|
||||
- `poetry run pytest -q`
|
||||
- `python3 spec/gen_spec_index.py --check`
|
||||
@@ -1,81 +0,0 @@
|
||||
---
|
||||
id: 030
|
||||
title: Add TBank payment notification webhook and CDEK order creation
|
||||
status: DONE
|
||||
created: 2026-04-18
|
||||
---
|
||||
|
||||
## Context
|
||||
TBank Init API уже получает `NotificationURL`, а данные заявки сохраняются в PostgreSQL после создания payment link. Сейчас приложение не принимает HTTP-уведомления TBank и не создаёт заказ в CDEK после подтверждения оплаты.
|
||||
|
||||
## Goal
|
||||
Добавить `POST /api/v1/delivery/tbank/notifications`, который принимает payment notification от TBank, проверяет token уведомления, при валидном статусе `CONFIRMED` находит сохранённую заявку по `OrderId` и регистрирует заказ в CDEK. Валидные уведомления с другими статусами должны подтверждаться без регистрации заказа в CDEK.
|
||||
|
||||
## Constraints
|
||||
- Controller отвечает только за routing, DTO validation и HTTP error mapping; endpoint вызывает ровно один метод Service: `AggregatorService.handle_tbank_payment_notification()`.
|
||||
- Service выполняет только orchestration: verification через TBank adapter, выбор действия через pure Business Logic, чтение/обновление заявки через OrderRepository и вызов injected CDEK order adapter.
|
||||
- Правило `CONFIRMED` -> создать заказ CDEK, остальные статусы -> подтвердить без CDEK должно быть pure Business Logic в `app/domain/`.
|
||||
- TBank-specific token verification должна быть инкапсулирована в `app/adapters/tbank/` и использовать `tbank_payment.auth.password`.
|
||||
- Проверка token MUST следовать контракту TBank HTTP-уведомлений: использовать top-level scalar fields кроме `Token`, не включать вложенные объекты (`Data`, `Receipt`), добавить `Password`, отсортировать ключи по алфавиту, конкатенировать значения, посчитать SHA-256 и сравнить с `Token`.
|
||||
- Успешно обработанное уведомление MUST возвращать `HTTP 200` с plain text body `OK`.
|
||||
- Валидные уведомления со статусами, отличными от `CONFIRMED`, MUST возвращать `OK` без чтения CDEK и без создания заказа.
|
||||
- Повторное валидное уведомление `CONFIRMED` для заявки с уже сохранённым `cdek_order_uuid` MUST возвращать `OK` без повторного вызова CDEK.
|
||||
- Если token невалиден, endpoint MUST возвращать deterministic `400` и не обращаться к repository или CDEK.
|
||||
- Если заявка для `OrderId` не найдена или CDEK registration не завершилась успешно, endpoint MUST не возвращать `OK`, чтобы TBank мог повторить notification delivery.
|
||||
- Repository содержит только CRUD/query/update primitives; без workflow logic и business decisions.
|
||||
- CDEK order payload MUST передавать `order_uuid` как external идентификатор заказа CDEK (поле `number` в CDEK order contract), чтобы повторные вызовы CDEK при HTTP timeout/ретрае обрабатывались идемпотентно на стороне CDEK и никогда не создавали дубль заказа.
|
||||
- Service MUST трактовать ответ CDEK "заказ с таким external id уже существует" (возврат существующего `entity.uuid` либо CDEK-specific duplicate response) как успех, сохранять возвращённый `cdek_order_uuid` и отвечать `OK`, а не как ошибку с повторной регистрацией.
|
||||
- Не изменять public contract `POST /api/v1/delivery/order`, price flow и address suggestion flow.
|
||||
- Не добавлять новые payment providers, refund flow, recurring payments, ручной retry endpoint или endpoint чтения заявок.
|
||||
- Не изменять файлы в `spec/`.
|
||||
|
||||
## Acceptance criteria
|
||||
- Существует schema `TBankPaymentNotification` с обязательными полями `TerminalKey`, `OrderId`, `Success`, `Status`, `PaymentId`, `ErrorCode`, `Amount`, `Token` и поддержкой дополнительных top-level полей TBank. `PaymentId` валидируется как положительное целое (TBank присылает long integer) и сохраняется в `tbank_payment_id` колонке `BIGINT`.
|
||||
- `POST /api/v1/delivery/tbank/notifications` реализован в существующем controller `app/controllers/v1/delivery.py` и возвращает `PlainTextResponse("OK")` при успешной обработке.
|
||||
- Controller делегирует обработку ровно в `AggregatorService.handle_tbank_payment_notification()` и не содержит branching по TBank status.
|
||||
- `TBankAdapter` умеет проверять token payment notification через `tbank_payment.auth.password`; невалидный token маппится в service/controller error path без repository/CDEK side effects.
|
||||
- В `app/domain/` есть pure function, которая для `Status == "CONFIRMED"`, `Success == true` и `ErrorCode == "0"` возвращает действие регистрации CDEK, а для остальных статусов возвращает действие acknowledge-only.
|
||||
- `OrderRepository` предоставляет primitive для получения заявки по `order_uuid`, сохранения последнего TBank payment status/payment id и сохранения `cdek_order_uuid`.
|
||||
- Таблица `orders` содержит минимум поля `payment_status`, `tbank_payment_id` (BIGINT), `cdek_order_uuid`, `updated_at` в исходной миграции; отдельная миграция не вводится, так как production БД ещё не развёрнута.
|
||||
- При валидном `CONFIRMED` notification service получает order по `OrderId`, реконструирует `InitPaymentRequest` из сохранённых данных, вызывает injected CDEK order adapter registration и сохраняет полученный `cdek_order_uuid`.
|
||||
- Если для `OrderId` уже сохранён `cdek_order_uuid`, повторный `CONFIRMED` notification возвращает `OK` без повторного вызова CDEK.
|
||||
- CDEK order mapper проставляет `InitPaymentRequest.order_uuid` в поле external номера заказа CDEK (`number`), так что повторный POST с тем же external id не создаёт второй заказ в CDEK.
|
||||
- Если CDEK create фактически завершился успешно, но сохранение `cdek_order_uuid` в DB не прошло, следующий валидный `CONFIRMED` notification MUST завершиться сохранением того же `cdek_order_uuid` (полученного по тому же external id) без создания второго заказа в CDEK.
|
||||
- Валидные notification со статусами `AUTHORIZED`, `REJECTED`, `CANCELED`, `DEADLINE_EXPIRED` и неизвестными status values возвращают `OK` без регистрации CDEK order.
|
||||
- Ошибка поиска заявки или ошибка CDEK registration возвращает deterministic non-OK HTTP response и логируется.
|
||||
- `NotificationURL` в runtime/test/example конфигурации, если он присутствует в репозитории, указывает на `/api/v1/delivery/tbank/notifications`.
|
||||
|
||||
## Definition of Done
|
||||
- [ ] Добавлена schema `TBankPaymentNotification`.
|
||||
- [ ] Добавлена pure Business Logic для выбора действия по TBank payment notification status.
|
||||
- [ ] Добавлена token verification logic в TBank adapter.
|
||||
- [ ] Расширена SQLAlchemy model и обновлена существующая Alembic migration `20260412_028_create_orders_table` payment/CDEK status fields (production БД отсутствует, новая миграция не нужна).
|
||||
- [ ] Расширен `OrderRepository` primitives для read/update операций, нужных webhook flow.
|
||||
- [ ] Реализован `AggregatorService.handle_tbank_payment_notification()` с DI dependencies.
|
||||
- [ ] Добавлен endpoint `POST /api/v1/delivery/tbank/notifications` в существующий delivery controller.
|
||||
- [ ] Wiring использует существующие `TBankAdapter`, `OrderRepository` и CDEK provider/adapter без provider HTTP details в Service/Controller.
|
||||
- [ ] Повторные `CONFIRMED` notification обрабатываются идемпотентно.
|
||||
- [ ] CDEK order mapper проставляет `order_uuid` в поле external номера заказа CDEK.
|
||||
- [ ] Service корректно обрабатывает ответ CDEK "заказ уже существует" как успех и сохраняет возвращённый `cdek_order_uuid`.
|
||||
- [ ] Валидные non-`CONFIRMED` statuses подтверждаются без CDEK side effects.
|
||||
- [ ] Обновлены tests для domain, adapter, repository, service, controller и smoke wiring.
|
||||
- [ ] Все команды из раздела Commands проходят.
|
||||
|
||||
## Tests
|
||||
- Добавить `tests/domain/test_payment_notifications.py`: `CONFIRMED` + `Success=true` + `ErrorCode=0` -> create CDEK order action; остальные known/unknown statuses -> acknowledge-only.
|
||||
- Обновить или добавить `tests/adapters/tbank/test_notifications.py`: valid token, invalid token, exclusion of `Token`, exclusion of nested `Data`/`Receipt`, inclusion of extra scalar fields, deterministic SHA-256 comparison.
|
||||
- Обновить `tests/repositories/order/test_repository.py`: получение заявки по `order_uuid`, сохранение `payment_status`/`tbank_payment_id`, сохранение `cdek_order_uuid`, duplicate/idempotency scenario.
|
||||
- Обновить `tests/adapters/delivery_providers/cdek/test_order_mapper.py`: CDEK order payload содержит `InitPaymentRequest.order_uuid` в поле external номера заказа (`number`).
|
||||
- Добавить `tests/services/test_tbank_notifications.py`: valid `CONFIRMED` вызывает repository lookup, CDEK registration и save `cdek_order_uuid`; duplicate `CONFIRMED` не вызывает CDEK; non-`CONFIRMED` возвращает `OK` без CDEK; invalid token не обращается к repository; missing order/CDEK failure возвращает service error; сценарий "CDEK create succeeded, save `cdek_order_uuid` raised" — следующий `CONFIRMED` notification вызывает CDEK повторно с тем же external id, получает тот же `cdek_order_uuid`, сохраняет его и возвращает `OK` (суммарно ровно один реальный заказ в CDEK).
|
||||
- Добавить `tests/controllers/v1/test_tbank_notifications.py`: endpoint возвращает plain text `OK` для successful service response, 400 для invalid token, 503 для temporary processing failure, controller делегирует ровно один service method.
|
||||
- Обновить `tests/smoke/test_app_import.py` при необходимости для проверки wiring нового endpoint.
|
||||
|
||||
## Commands
|
||||
- `poetry run pytest tests/domain/test_payment_notifications.py -q`
|
||||
- `poetry run pytest tests/adapters/tbank/test_notifications.py -q`
|
||||
- `poetry run pytest tests/repositories/order/test_repository.py -q`
|
||||
- `poetry run pytest tests/services/test_tbank_notifications.py -q`
|
||||
- `poetry run pytest tests/controllers/v1/test_tbank_notifications.py -q`
|
||||
- `poetry run pytest tests/smoke/test_app_import.py -q`
|
||||
- `poetry run pytest -q`
|
||||
- `python3 spec/gen_spec_index.py --check`
|
||||
@@ -1,71 +0,0 @@
|
||||
---
|
||||
id: 031
|
||||
title: Validate init-payment price with CDEK tariff
|
||||
status: TODO
|
||||
created: 2026-04-18
|
||||
---
|
||||
|
||||
## Context
|
||||
`POST /api/v1/delivery/order` сейчас принимает `price` от клиента и использует это значение как сумму платежа TBank. Клиент может передать произвольную сумму, не соответствующую тарифу CDEK.
|
||||
|
||||
## Goal
|
||||
Перед созданием ссылки на оплату валидировать `InitPaymentRequest.price` через CDEK `POST /calculator/tarifflist` для данных заявки. Если сумма не совпадает с валидированной суммой CDEK, не создавать payment link и вернуть детерминированную ошибку.
|
||||
|
||||
## Constraints
|
||||
- Controller не должен содержать business logic или provider-specific branching.
|
||||
- Endpoint `POST /api/v1/delivery/order` должен по-прежнему вызывать ровно один метод Service: `AggregatorService.init_payment()`.
|
||||
- Service выполняет только orchestration: вызывает injected CDEK adapter для расчёта цены, делегирует сравнение в Business Logic, затем вызывает TBank adapter только при успешной валидации.
|
||||
- Pure правило сравнения цены должно жить в `app/domain/`: сравнение выполняется в копейках, с применением существующего provider price multiplier и округления `ROUND_HALF_UP`.
|
||||
- Внешний IO для CDEK должен оставаться только в CDEK adapter; TBank-specific logic остаётся только в TBank adapter.
|
||||
- CDEK validation MUST использовать существующий endpoint CDEK `POST /calculator/tarifflist`; запрещено использовать `/orders`, `/calculator/tariff` или registration flow для проверки цены.
|
||||
- CDEK adapter должен переиспользовать существующие auth, retry, timeout, HTTP error handling и response mapping для tariff list там, где это совместимо с `InitPaymentRequest`.
|
||||
- CDEK validation должна использовать данные существующего `InitPaymentRequest`: `tariff_code`, `from_location`, `to_location`, `packages` и `services` при наличии.
|
||||
- Публичный request/response contract `POST /api/v1/delivery/order` не менять, кроме ужесточения runtime validation поля `price`.
|
||||
- При mismatch цены нельзя вызывать TBank adapter и нельзя сохранять заявку в PostgreSQL.
|
||||
- При ошибке CDEK validation нельзя вызывать TBank adapter и нельзя сохранять заявку в PostgreSQL.
|
||||
- Не изменять price flow, address suggestion flow, payment notification webhook flow и CDEK order registration flow.
|
||||
- Не добавлять новые endpoints, payment providers, ручной retry endpoint или endpoint чтения заявок.
|
||||
- Не изменять файлы в `spec/`.
|
||||
|
||||
## Acceptance criteria
|
||||
- CDEK adapter предоставляет service-facing метод для расчёта валидной цены доставки по `InitPaymentRequest` через `POST /calculator/tarifflist` без регистрации заказа в CDEK.
|
||||
- Новый метод переиспользует существующую tariff-list инфраструктуру CDEK adapter: OAuth2 token, retry/timeout policy, handling 4xx/5xx/transport errors и mapping `tariff_codes` в internal price model.
|
||||
- CDEK validation request использует `request.tariff_code`, `from_location`, `to_location`, `packages` и `services` из `InitPaymentRequest`.
|
||||
- CDEK validation request отправляется на тот же configured base URL path `/calculator/tarifflist`, который используется текущим CDEK price flow.
|
||||
- Если CDEK не возвращает цену для `request.tariff_code`, service завершает `init_payment()` детерминированной `InvalidInitPaymentRequestError`.
|
||||
- Business Logic рассчитывает expected payment amount в копейках: provider price в `RUB` после существующего multiplier и `ROUND_HALF_UP` конвертируется в копейки и сравнивается с `InitPaymentRequest.price`.
|
||||
- Если `InitPaymentRequest.price` не равен expected amount, `AggregatorService.init_payment()` поднимает `InvalidInitPaymentRequestError`.
|
||||
- При успешной price validation `AggregatorService.init_payment()` вызывает TBank adapter с исходным `order_uuid` и валидированным `price`.
|
||||
- При успешной price validation и успешном TBank response сохранение заявки в PostgreSQL остаётся прежним.
|
||||
- При CDEK provider request error service маппит ошибку в `InvalidInitPaymentRequestError`.
|
||||
- При CDEK transport, timeout или 5xx error service маппит ошибку в `InitPaymentUnavailableError`.
|
||||
- Controller маппит `InvalidInitPaymentRequestError` в HTTP 400 и `InitPaymentUnavailableError` в HTTP 503 по текущему error contract.
|
||||
- Tests подтверждают, что при mismatch цены TBank adapter и OrderRepository не вызываются.
|
||||
|
||||
## Definition of Done
|
||||
- [ ] Добавлен service-facing CDEK adapter method для расчёта цены по `InitPaymentRequest` через `POST /calculator/tarifflist`.
|
||||
- [ ] Метод переиспользует существующие auth, retry, timeout, error handling и tariff response mapping CDEK adapter.
|
||||
- [ ] Добавлена pure Business Logic для сравнения requested price с CDEK validated price в копейках.
|
||||
- [ ] `AggregatorService.init_payment()` выполняет CDEK price validation до вызова TBank adapter.
|
||||
- [ ] Ошибки CDEK validation маппятся в существующие service/controller error paths.
|
||||
- [ ] TBank adapter вызывается только после успешной CDEK price validation.
|
||||
- [ ] OrderRepository вызывается только после успешной CDEK price validation и успешного получения `payment_url`.
|
||||
- [ ] Публичный contract `POST /api/v1/delivery/order` не изменён, кроме runtime rejection невалидной цены.
|
||||
- [ ] Обновлены tests для domain, CDEK adapter, service и controller.
|
||||
- [ ] Все команды из раздела Commands проходят.
|
||||
|
||||
## Tests
|
||||
- Добавить или обновить `tests/domain/test_payment_price_validation.py`: exact match, mismatch, multiplier с `ROUND_HALF_UP`, конвертация `RUB` в копейки, не-`RUB` currency как deterministic validation failure.
|
||||
- Добавить или обновить `tests/adapters/delivery_providers/cdek/test_payment_price_validation.py`: validation request отправляется на `/calculator/tarifflist`; payload строится из `InitPaymentRequest`, используется `tariff_code`, `packages`, `services`, `from_location`, `to_location`; success/error mapping покрыт stubs/mocks.
|
||||
- Обновить `tests/services/test_init_payment.py`: success вызывает CDEK validation до TBank; mismatch возвращает `InvalidInitPaymentRequestError` без TBank и repository calls; CDEK request error маппится в `InvalidInitPaymentRequestError`; CDEK client error маппится в `InitPaymentUnavailableError`.
|
||||
- Обновить `tests/controllers/v1/test_init_payment.py`: price mismatch возвращает HTTP 400; временная ошибка CDEK validation возвращает HTTP 503; controller продолжает делегировать ровно один service method.
|
||||
- При необходимости обновить `tests/smoke/test_app_import.py` для wiring нового CDEK validation dependency.
|
||||
|
||||
## Commands
|
||||
- `poetry run pytest tests/domain/test_payment_price_validation.py -q`
|
||||
- `poetry run pytest tests/adapters/delivery_providers/cdek/test_payment_price_validation.py -q`
|
||||
- `poetry run pytest tests/services/test_init_payment.py -q`
|
||||
- `poetry run pytest tests/controllers/v1/test_init_payment.py -q`
|
||||
- `poetry run pytest tests/smoke/test_app_import.py -q`
|
||||
- `poetry run pytest -q`
|
||||
- `python3 spec/gen_spec_index.py --check`
|
||||
@@ -1,102 +0,0 @@
|
||||
---
|
||||
id: 032
|
||||
title: Rework init-payment contract to camelCase and structured address/contact
|
||||
status: TODO
|
||||
created: 2026-05-13
|
||||
---
|
||||
|
||||
## Context
|
||||
Фронт переходит на новый контракт ручки `POST /api/v1/delivery/order`. В нём
|
||||
адрес/контакт структурированы по-новому, появились флаг юр-лица и реквизиты
|
||||
(`isCompany`/`companyName`/`inn`/`kpp`/`phoneExt`), описание груза и вес
|
||||
(`content.description`), даты `pickupDate`/`deliveryDate`,
|
||||
`accountEmail`, блок `systemData` с зафиксированным тарифом, parcelType,
|
||||
docPackaging и dimensions. Поле наименования — camelCase. Это breaking change
|
||||
без обратной совместимости.
|
||||
|
||||
## Goal
|
||||
Привести бэкенд к новому контракту `/api/v1/delivery/order`, сохранив текущий
|
||||
flow «валидация цены через CDEK → создание payment URL в TBank → persist в
|
||||
PostgreSQL → возврат `payment_url`; webhook `CONFIRMED` → регистрация заказа в
|
||||
CDEK».
|
||||
|
||||
## Constraints
|
||||
- camelCase в API; внутренние имена остаются Python-friendly через Pydantic
|
||||
alias-generator. JSON принимается ТОЛЬКО в camelCase.
|
||||
- `orderUuid` и `systemData.tariff.tariffCode` приходят от фронта.
|
||||
- `systemData.tariff.price` — итоговая сумма в копейках; backend использует её
|
||||
как `amount_kopecks` для TBank без пересчёта.
|
||||
- Расширить `senderAddress` и `receiverAddress` обязательным полем `cityId: int`
|
||||
(то же, что в `/price`), чтобы резолвить CDEK city code из `cities_map`.
|
||||
- При `isCompany=true` поля `companyName`, `inn`, `kpp` обязательны.
|
||||
- При `parcelType='doc'` `dimensions` принимает значение `null`; CDEK packages
|
||||
отправляются без length/width/height.
|
||||
- При `parcelType='parcel'` `dimensions` обязателен.
|
||||
- Адрес для CDEK собирается строкой `"{city}, {street}, {house}, кв. {apartment}"`;
|
||||
если `apartment` пустой/отсутствует — без хвоста `, кв. ...`.
|
||||
- `pickupDate` → CDEK `shipment_point.date`; `deliveryDate` → CDEK
|
||||
`delivery_point.date` (если задан).
|
||||
- `content.description` пробрасывается в CDEK `packages[0].items[0].name` и
|
||||
`comment` верхнего уровня.
|
||||
- `phoneExt` пробрасывается в CDEK `phones[0].additional`.
|
||||
- Сохранять весь принятый payload в JSONB-колонке `payload` записи `orders`.
|
||||
- ORM-модель `orders` рефакторится: вместо колонок `sender/recipient/
|
||||
from_location/to_location/packages/services/comment/delivery_type` —
|
||||
одна колонка `payload JSONB NOT NULL`, плюс `account_email VARCHAR`.
|
||||
Колонки `order_uuid`, `payment_url`, `price`, `tariff_code`, `payment_status`,
|
||||
`tbank_payment_id`, `cdek_order_uuid`, `created_at`, `updated_at` сохраняются.
|
||||
|
||||
## Acceptance criteria
|
||||
- `POST /api/v1/delivery/order` принимает новый camelCase payload и возвращает
|
||||
`InitPaymentResponse` без изменений (`{ "payment_url": str }`).
|
||||
- Pydantic-модели валидируют: обязательность реквизитов юр-лица при
|
||||
`isCompany=true`; `parcelType='doc'` ⇒ `dimensions=null`/отсутствует;
|
||||
`parcelType='parcel'` ⇒ `dimensions` обязателен; `price > 0`.
|
||||
- Service вызывает `payment_price_validation_adapter.get_payment_price(request)`
|
||||
и `payment_adapter.create_payment_link(order_uuid, amount_kopecks)` с
|
||||
`amount_kopecks = request.systemData.tariff.price`.
|
||||
- CDEK order mapper из нового `InitPaymentRequest` формирует payload с полями:
|
||||
`number`, `type=2`, `tariff_code`, `sender`, `recipient`,
|
||||
`from_location.code/address/postal_code`, `to_location.code/address/postal_code`,
|
||||
`packages[0]` с `weight` (граммы) и опциональными dimensions,
|
||||
`packages[0].items` с описанием груза, `shipment_point.date`,
|
||||
`delivery_point.date` (если есть), `comment`.
|
||||
- При `isCompany=true` CDEK получает `contragent_type="LEGAL_ENTITY"`, `company`,
|
||||
`inn`, `kpp` для соответствующей стороны.
|
||||
- ORM `Order` хранит весь payload в `payload` JSONB; репозиторий сохраняет и
|
||||
читает его без потерь; миграция переводит таблицу со старой структуры на
|
||||
новую (drop старые колонки, добавить `payload`, `account_email`).
|
||||
|
||||
## Definition of Done
|
||||
- [ ] Pydantic-модели `payment.py` переписаны под новый camelCase-контракт с
|
||||
обязательными валидациями.
|
||||
- [ ] CDEK order mapper и `_build_payment_price_payload` собирают payload из
|
||||
новой структуры.
|
||||
- [ ] `AggregatorService.init_payment` использует `systemData.tariff.price` и
|
||||
`orderUuid` напрямую; persist использует обновлённый `OrderData`.
|
||||
- [ ] ORM-модель `Order`, `OrderData` и репозиторий перешли на `payload` JSONB
|
||||
и `account_email`.
|
||||
- [ ] Alembic-миграция `20260513_032_rework_orders_payload.py` применяется и
|
||||
откатывается.
|
||||
- [ ] Тесты на schemas, mapper, payment validation, controller и service
|
||||
обновлены и проходят.
|
||||
- [ ] `http-client.http` обновлён под новый payload.
|
||||
|
||||
## Tests
|
||||
- `tests/controllers/v1/test_init_payment.py` — fixture/assertion обновлены под
|
||||
camelCase; добавлены кейсы валидации юр-лица и doc/parcel dimensions.
|
||||
- `tests/adapters/delivery_providers/cdek/test_order_mapper.py` —
|
||||
mapping `phoneExt → additional`, контрагент юр-лица, address composition,
|
||||
doc без dimensions, shipment/delivery point dates.
|
||||
- `tests/adapters/delivery_providers/cdek/test_payment_price_validation.py` —
|
||||
validation payload собирается из `senderAddress.cityId`, `systemData.weight`,
|
||||
`systemData.dimensions`.
|
||||
- `tests/services/test_init_payment.py` — `init_payment` берёт сумму из
|
||||
`systemData.tariff.price`, `orderUuid` пробрасывается.
|
||||
- `tests/repositories/order/test_repository.py` (если есть) — обновлён под
|
||||
новую модель `Order`.
|
||||
|
||||
## Commands
|
||||
- `pytest tests/`
|
||||
- `alembic upgrade head` (smoke на пустой базе)
|
||||
- `python3 spec/gen_spec_index.py --check`
|
||||
@@ -1,94 +0,0 @@
|
||||
---
|
||||
id: 033
|
||||
title: Add CDEK waybill polling worker
|
||||
status: DONE
|
||||
created: 2026-05-23
|
||||
---
|
||||
|
||||
## Context
|
||||
`POST /v2/orders` в CDEK асинхронный. Когда `entity.related_entities[type=waybill].uuid`
|
||||
синхронно приходит в ответе, поле `url` практически всегда пустое — PDF генерится
|
||||
позже и его url доступен только при `GET /v2/print/orders/{waybill_uuid}` после
|
||||
перехода накладной в `READY`. Бывают и случаи, когда waybill_uuid тоже не приходит
|
||||
синхронно, или заказ уходит в терминальный `INVALID` без генерации накладной.
|
||||
Ранее `_save_cdek_order_uuid` (`app/services/aggregator.py`) писал waybill-поля из
|
||||
синхронного ответа POST, из-за чего запись прыгала между двумя источниками и
|
||||
требовала покрытия граничных случаев в двух местах.
|
||||
|
||||
## Goal
|
||||
Перевести запись waybill-данных под единоличную ответственность отдельного
|
||||
фонового worker-а. При регистрации заказа сохранять только `cdek_order_uuid`,
|
||||
а waybill_uuid / waybill_url пополнять поллингом до тех пор, пока заказ не
|
||||
дойдёт до waybill_url IS NOT NULL либо до терминального статуса.
|
||||
|
||||
## Constraints
|
||||
- Запуск — отдельный процесс `python -m app.workers.waybill_poller`, отдельный
|
||||
сервис в `docker-compose.yml`, одна реплика.
|
||||
- Никаких записей waybill-полей при регистрации заказа: `_save_cdek_order_uuid`
|
||||
должен сохранять только `cdek_order_uuid`.
|
||||
- Поллер — единственный writer полей `cdek_order_status`, `cdek_waybill_uuid`,
|
||||
`cdek_waybill_url`, `cdek_polled_at`.
|
||||
- На терминальных статусах заказа (`INVALID`, `DELIVERED`, `NOT_DELIVERED`,
|
||||
`CANCELLED`) опросы по этому заказу прекращаются.
|
||||
- Бизнес-логика терминальности — pure функция в `app/domain/cdek_polling.py`,
|
||||
без зависимостей на репозиторий/адаптер.
|
||||
- Соблюсти слои AGENTS.md: новые HTTP-вызовы CDEK живут только в адаптере,
|
||||
работа с БД — только в репозитории, оркестрация — в сервисе.
|
||||
|
||||
## Acceptance criteria
|
||||
- При успешной регистрации заказа в БД заполняется только `cdek_order_uuid`;
|
||||
`cdek_waybill_uuid` и `cdek_waybill_url` остаются `NULL`.
|
||||
- Worker раз в `waybill_poller.interval_seconds` секунд выбирает заказы с
|
||||
`cdek_order_uuid IS NOT NULL AND cdek_waybill_url IS NULL` и нетерминальным
|
||||
`cdek_order_status` (с упорядочиванием `cdek_polled_at NULLS FIRST` и лимитом
|
||||
`waybill_poller.batch_size`).
|
||||
- Если у заказа `cdek_waybill_uuid IS NULL`, worker делает `GET /v2/orders/{uuid}`
|
||||
и сохраняет последний статус заказа + waybill_uuid из `related_entities`.
|
||||
- Если `cdek_waybill_uuid IS NOT NULL` и url пуст, worker делает
|
||||
`GET /v2/print/orders/{waybill_uuid}` и сохраняет `entity.url`.
|
||||
- Ошибка CDEK по одному заказу логируется и не валит весь батч.
|
||||
- Worker корректно завершает работу по SIGTERM/SIGINT: закрывает HTTP-клиент и
|
||||
async engine.
|
||||
|
||||
## Definition of Done
|
||||
- [ ] Миграция `20260524_034_add_cdek_polling_columns.py` добавляет
|
||||
`cdek_order_status` и `cdek_polled_at`; модель синхронизирована.
|
||||
- [ ] `_save_cdek_order_uuid` и `OrderRepository.mark_cdek_order_registered`
|
||||
больше не принимают waybill-поля.
|
||||
- [ ] `CDEKClient.get_order` / `get_waybill` и соответствующие мапперы
|
||||
(`map_cdek_order_info_response`, `map_cdek_waybill_info_response`,
|
||||
dataclasses `CDEKOrderInfo`, `CDEKWaybillInfo`) реализованы; `CDEKProvider`
|
||||
предоставляет обёртки.
|
||||
- [ ] `app/domain/cdek_polling.py` содержит `TERMINAL_ORDER_STATUSES` и
|
||||
`is_terminal_order_status`.
|
||||
- [ ] `OrderRepository.list_orders_pending_waybill`, `record_order_poll`,
|
||||
`record_waybill_poll` реализованы.
|
||||
- [ ] `WaybillPollerService.poll_once` и `run_forever` реализованы.
|
||||
- [ ] `app/workers/waybill_poller.py` поднимает зависимости, поддерживает
|
||||
graceful shutdown по сигналу.
|
||||
- [ ] `config.example.yaml` содержит секцию `waybill_poller`, `app/config.py` —
|
||||
`WaybillPollerConfig`.
|
||||
- [ ] `docker-compose.yml` содержит сервис `waybill-poller`.
|
||||
|
||||
## Tests
|
||||
- `tests/domain/test_cdek_polling.py` — терминальные/нетерминальные статусы.
|
||||
- `tests/adapters/delivery_providers/cdek/test_order_mapper.py` — мапперы
|
||||
ответов GET /v2/orders/{uuid} и GET /v2/print/orders/{uuid}.
|
||||
- `tests/adapters/delivery_providers/cdek/test_order_info_client.py` —
|
||||
`CDEKClient.get_order` и `get_waybill`: 200/4xx/5xx, ретраи, парсинг.
|
||||
- `tests/repositories/order/test_repository.py` — `list_orders_pending_waybill`,
|
||||
`record_order_poll`, `record_waybill_poll` (фильтр, упорядочивание, защита
|
||||
waybill_uuid/waybill_url от перезаписи).
|
||||
- `tests/services/test_waybill_poller.py` — `poll_once` на стабах (два пути,
|
||||
терминальный статус, изоляция ошибок), `run_forever` завершается по
|
||||
stop_event.
|
||||
- `tests/services/test_tbank_notifications.py` — регистрация заказа больше не
|
||||
сохраняет waybill-поля.
|
||||
- `tests/workers/test_waybill_poller_main.py` — `_run` собирает зависимости и
|
||||
завершается по stop_event.
|
||||
|
||||
## Commands
|
||||
- `poetry run pytest -q`
|
||||
- `poetry run alembic upgrade head`
|
||||
- `poetry run python -m app.workers.waybill_poller`
|
||||
- `python3 spec/gen_spec_index.py --check`
|
||||
@@ -1,87 +0,0 @@
|
||||
---
|
||||
id: 034
|
||||
title: Add CDEK waybill e-mail sender worker
|
||||
status: DONE
|
||||
created: 2026-05-23
|
||||
---
|
||||
|
||||
## Context
|
||||
После задачи 033 фоновый `WaybillPollerService` дотягивает у CDEK
|
||||
`cdek_waybill_url` для зарегистрированных заказов и на этом цепочка
|
||||
обрывается: PDF-накладная остаётся доступной только по URL, клиенту на почту
|
||||
не отправляется. Адрес получателя уже сохраняется в `Order.account_email`
|
||||
(приходит из `InitPaymentRequest.account_email`).
|
||||
|
||||
## Goal
|
||||
Добавить отдельный фоновый сервис и worker, который для заказов с
|
||||
`cdek_waybill_url IS NOT NULL AND waybill_email_sent_at IS NULL` скачивает PDF
|
||||
накладной у CDEK и отправляет его вложением на `Order.account_email`.
|
||||
|
||||
## Constraints
|
||||
- Запуск — отдельный процесс `python -m app.workers.waybill_email_sender`,
|
||||
отдельный сервис в `docker-compose.yml`, одна реплика, тот же
|
||||
`config.yaml`, что и у `waybill-poller`.
|
||||
- Поллер waybill-данных (033) не меняем; новый сервис — единственный writer
|
||||
колонки `waybill_email_sent_at`.
|
||||
- Соблюдение слоёв AGENTS.md: SMTP — только в адаптере, скачивание PDF —
|
||||
в CDEK-адаптере, работа с БД — в репозитории, оркестрация — в сервисе.
|
||||
- Текст письма (тема, тело, имя вложения) — приватные константы сервиса,
|
||||
без отдельного domain-модуля и без шаблонов в config.
|
||||
- На вход (POST /api/v1/delivery/order) `accountEmail` валидируется как
|
||||
`pydantic.EmailStr` — заведомо некорректный адрес отбрасывается на
|
||||
контроллере и не доходит до воркера.
|
||||
|
||||
## Acceptance criteria
|
||||
- При появлении у заказа `cdek_waybill_url` worker в течение
|
||||
`waybill_email_sender.interval_seconds` скачивает PDF и отправляет письмо
|
||||
на `Order.account_email`, после чего проставляет `waybill_email_sent_at`.
|
||||
- Письмо содержит тему `Накладная по заказу {order_uuid}`, тело — краткое
|
||||
уведомление со ссылкой на `cdek_waybill_url`, и вложение
|
||||
`waybill_{order_uuid}.pdf` (`application/pdf`).
|
||||
- Повторный запуск worker-а не приводит к повторной отправке (фильтр
|
||||
`waybill_email_sent_at IS NULL`).
|
||||
- Ошибка скачивания PDF или SMTP по одному заказу логируется и не валит
|
||||
батч; заказ остаётся pending и будет переотправлен в следующем тике.
|
||||
- `POST /api/v1/delivery/order` с заведомо невалидным `accountEmail`
|
||||
возвращает 422.
|
||||
- Worker корректно завершает работу по SIGTERM/SIGINT: закрывает HTTP-
|
||||
клиент и async engine.
|
||||
|
||||
## Definition of Done
|
||||
- [ ] Миграция `20260524_035_add_waybill_email_sent_at.py` добавляет
|
||||
`waybill_email_sent_at`; модель синхронизирована.
|
||||
- [ ] `OrderRepository.list_orders_pending_waybill_email` и
|
||||
`record_waybill_email_sent` реализованы.
|
||||
- [ ] `CDEKClient.download_waybill_pdf(url)` реализован.
|
||||
- [ ] Адаптер `app/adapters/email/smtp_client.py` (`aiosmtplib`) реализован.
|
||||
- [ ] `WaybillEmailSenderService.poll_once` и `run_forever` реализованы.
|
||||
- [ ] `app/workers/waybill_email_sender.py` поднимает зависимости и
|
||||
поддерживает graceful shutdown.
|
||||
- [ ] `config.example.yaml` содержит секции `email` и
|
||||
`waybill_email_sender`; `app/config.py` — `EmailAdapterConfig` и
|
||||
`WaybillEmailSenderConfig`.
|
||||
- [ ] `docker-compose.yml` содержит сервис `waybill-email-sender`.
|
||||
- [ ] `pyproject.toml` содержит `aiosmtplib` и `email-validator`.
|
||||
- [ ] `InitPaymentRequest.account_email` имеет тип `EmailStr`.
|
||||
|
||||
## Tests
|
||||
- `tests/adapters/email/test_smtp_client.py` — корректное MIME-сообщение,
|
||||
вложение PDF, проброс SMTP-ошибок.
|
||||
- `tests/adapters/delivery_providers/cdek/test_waybill_download_client.py` —
|
||||
`download_waybill_pdf`: 200 → bytes, 4xx/5xx, ретраи, auth header.
|
||||
- `tests/repositories/order/test_repository.py` — расширить:
|
||||
`list_orders_pending_waybill_email` (фильтр и порядок по `created_at`),
|
||||
`record_waybill_email_sent` (защита от перезаписи).
|
||||
- `tests/services/test_waybill_email_sender.py` — `poll_once` на стабах:
|
||||
успешная отправка, ошибка скачивания PDF, ошибка SMTP, изоляция ошибок
|
||||
между заказами, `run_forever` завершается по `stop_event`.
|
||||
- `tests/workers/test_waybill_email_sender_main.py` — `_run` собирает
|
||||
зависимости и завершается по `stop_event`.
|
||||
- `tests/controllers/v1/test_init_payment.py` — 422 на невалидный
|
||||
`accountEmail`.
|
||||
|
||||
## Commands
|
||||
- `poetry run pytest -q`
|
||||
- `poetry run alembic upgrade head`
|
||||
- `poetry run python -m app.workers.waybill_email_sender`
|
||||
- `python3 spec/gen_spec_index.py --check`
|
||||
@@ -1,44 +0,0 @@
|
||||
---
|
||||
id: 035
|
||||
title: Generalize tariff_code to string identifier
|
||||
status: TODO
|
||||
created: 2026-05-30
|
||||
---
|
||||
|
||||
## Context
|
||||
Добавляется второй провайдер доставки CSE («Карго»), который идентифицирует тариф
|
||||
GUID-строкой. Текущие схемы и persistence используют числовой `tariff_code`
|
||||
(`int`), несовместимый с CSE. Требуется обобщить идентификатор тарифа до строки для
|
||||
обоих провайдеров.
|
||||
|
||||
## Goal
|
||||
Перевести `tariff_code` на строковый тип в API-схемах и в persistence, сохранив
|
||||
обратное поведение CDEK (числовой код передаётся как строка).
|
||||
|
||||
## Constraints
|
||||
- Не изменять бизнес-правила в `app/domain/`.
|
||||
- CDEK adapter должен формировать `tariff_code` как строку из своего числового кода.
|
||||
- Сохранить совместимость существующего init-payment контракта, кроме типа
|
||||
`tariff_code`.
|
||||
- Изменение типа колонки выполнить миграцией Alembic.
|
||||
- Не изменять файлы в `spec/`.
|
||||
|
||||
## Acceptance criteria
|
||||
- `DeliveryPrice.tariff_code: str | None`.
|
||||
- `SystemDataTariff.tariff_code: str` с непустым значением.
|
||||
- CDEK `mapper`/`order_mapper` корректно работают со строковым `tariff_code`.
|
||||
- Колонка `orders.tariff_code` имеет строковый тип; добавлена миграция.
|
||||
- Существующие тесты обновлены под строковый `tariff_code`.
|
||||
|
||||
## Definition of Done
|
||||
- [ ] Обновлены схемы `DeliveryPrice` и `SystemDataTariff`.
|
||||
- [ ] Обновлены CDEK мапперы.
|
||||
- [ ] Обновлены модель `Order` и миграция Alembic.
|
||||
- [ ] Затронутые тесты проходят.
|
||||
|
||||
## Tests
|
||||
- Обновить unit-тесты CDEK мапперов и схем.
|
||||
- Проверить миграцию на тестовой БД.
|
||||
|
||||
## Commands
|
||||
- `poetry run pytest -q`
|
||||
@@ -1,58 +0,0 @@
|
||||
---
|
||||
id: 036
|
||||
title: Add CSE SOAP delivery provider adapter
|
||||
status: TODO
|
||||
created: 2026-05-30
|
||||
---
|
||||
|
||||
## Context
|
||||
Нужен второй провайдер доставки CSE (система «Карго»). CSE предоставляет SOAP/1C
|
||||
web-сервис (namespace `http://www.cargo3.ru`, endpoint `Web1C.1cws`), авторизация —
|
||||
`login`/`password` строковыми параметрами в теле запроса. Данные передаются
|
||||
универсальными структурами `Element`/`Row`; справочные значения — GUID. Спецификация
|
||||
методов: `Calc` (расчёт стоимости) и `SaveDocuments` (регистрация заказа). Поллинг
|
||||
статуса и получение PDF в этой задаче не реализуются.
|
||||
|
||||
## Goal
|
||||
Реализовать adapter-пакет CSE: SOAP-транспорт и сериализация `Element`,
|
||||
`CSEClient` с методами `calc()`/`save_order()`, мапперы расчёта и регистрации, и
|
||||
`CSEProvider`, реализующий контракт `DeliveryProvider` плюс расчёт цены для
|
||||
валидации платежа и регистрацию заказа.
|
||||
|
||||
## Constraints
|
||||
- Только Adapter layer; без business decisions, фильтрации и сортировки.
|
||||
- Весь XML/HTTP IO инкапсулирован в пакете `cse/`.
|
||||
- Параметры CSE (`base_url`, `login`, `password`, retry policy, timeout, cache TTL)
|
||||
приходят из секции `adapter` YAML-конфига; запрещён хардкод credentials.
|
||||
- Ошибки CSE (`Properties.Error` → `List[Description]`) маппятся в иерархию
|
||||
`ProviderClientError`/`ProviderRequestError`.
|
||||
- Валюта `RUR` нормализуется в `RUB`; цена — `Decimal` в рублях.
|
||||
- Не изменять файлы в `spec/`.
|
||||
|
||||
## Acceptance criteria
|
||||
- Существует пакет `app/adapters/delivery_providers/cse/` с SOAP-сериализацией,
|
||||
клиентом, мапперами и `CSEProvider` (`name = "cse"`).
|
||||
- `CSEProvider.get_prices` возвращает унифицированные `DeliveryPrice` из ответа
|
||||
`Calc` с `provider="cse"` и строковым `tariff_code` (GUID).
|
||||
- `CSEProvider.get_payment_price` возвращает выбранный тариф для валидации платежа.
|
||||
- `CSEProvider.register_order` выполняет `SaveDocuments` (`DocumentType=Order`) и
|
||||
возвращает результат регистрации с номером документа.
|
||||
- Адаптер использует значения из секции `adapter` YAML-конфига, включая
|
||||
`cse_timeout_seconds` и `cse_cache_ttl_seconds`.
|
||||
- Добавлены поля `cse_*` в `AdapterConfig` и шаблоны конфигов.
|
||||
|
||||
## Definition of Done
|
||||
- [ ] Реализованы `soap`, `client`, `errors`, `mapper`, `order_mapper`, `CSEProvider`.
|
||||
- [ ] Добавлены `cse_*` поля в `AdapterConfig`, `config.template.yaml`,
|
||||
`config.test.yaml`.
|
||||
- [ ] Adapter unit-тесты покрывают сериализацию/парсинг, mapping `Calc`/`SaveDocuments`
|
||||
и обработку ошибок.
|
||||
|
||||
## Tests
|
||||
- Unit-тесты SOAP-сериализации и парсинга `Element`.
|
||||
- Unit-тесты мапперов на XML-примерах из спецификации.
|
||||
- Тесты применения параметров CSE из секции `adapter`.
|
||||
- Для внешних HTTP взаимодействий использовать stubs/mocks.
|
||||
|
||||
## Commands
|
||||
- `poetry run pytest tests/adapters/delivery_providers/cse -q`
|
||||
@@ -1,39 +0,0 @@
|
||||
---
|
||||
id: 037
|
||||
title: Add CSE geography to cities map
|
||||
status: TODO
|
||||
created: 2026-05-30
|
||||
---
|
||||
|
||||
## Context
|
||||
Методы CSE `Calc` и `SaveDocuments` требуют географию отправления/доставки в виде
|
||||
GUID (либо ФИАС/postcode). В проекте сопоставление город → коды провайдера хранится
|
||||
в `app/cities.py` (блок `cdek` на каждый город). Для CSE требуется аналогичный блок
|
||||
`cse` с GUID географии.
|
||||
|
||||
## Goal
|
||||
Расширить `cities_map` данными географии CSE и зафиксировать справочные константы
|
||||
CSE (вид груза `TypeOfCargo`, валюта `RUR`), необходимые адаптеру для запросов.
|
||||
|
||||
## Constraints
|
||||
- Не изменять структуру существующего блока `cdek`.
|
||||
- Города без поддержки CSE имеют `cse: None`.
|
||||
- Константы вида груза/валюты держать в пакете адаптера `cse/` или в конфиге, без
|
||||
бизнес-логики.
|
||||
- Не изменять файлы в `spec/`.
|
||||
|
||||
## Acceptance criteria
|
||||
- Каждый город в `cities_map` содержит блок `cse` (GUID географии) или `None`.
|
||||
- Маппинг `parcel_type` (`doc`/`parcel`) → `TypeOfCargo` GUID и GUID валюты `RUR`
|
||||
определены и используются адаптером CSE.
|
||||
|
||||
## Definition of Done
|
||||
- [ ] `cities_map` дополнен блоком `cse`.
|
||||
- [ ] Определены константы вида груза и валюты CSE.
|
||||
- [ ] Тесты подтверждают чтение географии CSE для поддерживаемого города.
|
||||
|
||||
## Tests
|
||||
- Unit-тесты сопоставления город → география CSE.
|
||||
|
||||
## Commands
|
||||
- `poetry run pytest -q -k cities`
|
||||
@@ -1,48 +0,0 @@
|
||||
---
|
||||
id: 038
|
||||
title: Route init-payment validation and registration by provider
|
||||
status: TODO
|
||||
created: 2026-05-30
|
||||
---
|
||||
|
||||
## Context
|
||||
С появлением второго провайдера CSE `AggregatorService` должен выбирать адаптер
|
||||
валидации цены и регистрации заказа по провайдеру выбранного тарифа. Сейчас сервис
|
||||
жёстко использует единственный CDEK-адаптер. Поле `provider` уже присутствует в
|
||||
`DeliveryPrice`/`SystemDataTariff`. Регистрация заказа выполняется при уведомлении
|
||||
об оплате TBank. Поллинг статуса и email в этой задаче не затрагиваются.
|
||||
|
||||
## Goal
|
||||
Маршрутизировать валидацию цены и регистрацию заказа по
|
||||
`request.system_data.tariff.provider`, подключить `CSEProvider` в DI рядом с CDEK,
|
||||
сохранять выбранного провайдера и номер заказа CSE в persistence.
|
||||
|
||||
## Constraints
|
||||
- Controller вызывает Service без бизнес-ветвления.
|
||||
- Service не содержит чистых бизнес-правил; маршрутизация — оркестрация.
|
||||
- Не изменять контракт API, кроме уже обобщённого `tariff_code`.
|
||||
- Колонки `cdek_*` не изменять; добавить `provider` и `cse_order_number` миграцией.
|
||||
- Не изменять файлы в `spec/`.
|
||||
|
||||
## Acceptance criteria
|
||||
- `GET /api/v1/delivery/calculate` возвращает агрегированные тарифы CDEK и CSE.
|
||||
- `AggregatorService` выбирает адаптер валидации/регистрации по имени провайдера
|
||||
тарифа; общий тип результата регистрации не привязан к CDEK.
|
||||
- При оплате заказа с тарифом CSE регистрация выполняется через `CSEProvider`;
|
||||
в `orders` сохранены `provider` и `cse_order_number`.
|
||||
- DI в контроллере доставки инстанцирует и подключает `CSEProvider`.
|
||||
|
||||
## Definition of Done
|
||||
- [ ] `AggregatorService` маршрутизирует по провайдеру (mapping адаптеров).
|
||||
- [ ] Обобщён тип результата регистрации заказа.
|
||||
- [ ] Модель `Order` и репозиторий дополнены (`provider`, `cse_order_number`,
|
||||
`mark_cse_order_registered`); добавлена миграция.
|
||||
- [ ] DI контроллера подключает CDEK и CSE.
|
||||
- [ ] Тесты маршрутизации валидации/регистрации по провайдеру проходят.
|
||||
|
||||
## Tests
|
||||
- Service unit-тесты маршрутизации (моки CDEK/CSE адаптеров).
|
||||
- Тесты persistence для `provider`/`cse_order_number`.
|
||||
|
||||
## Commands
|
||||
- `poetry run pytest tests/services -q`
|
||||
Reference in New Issue
Block a user