upload
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.log
|
||||
.pytest_cache/
|
||||
.mypy_cache/
|
||||
.venv/
|
||||
venv/
|
||||
openapi/
|
||||
README.md
|
||||
@@ -0,0 +1,14 @@
|
||||
FROM python:3.12-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY app ./app
|
||||
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
EXPOSE 8010
|
||||
|
||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8010"]
|
||||
@@ -0,0 +1,83 @@
|
||||
# demo-backend
|
||||
|
||||
Отдельный демо backend для travel pipeline из `openapi/travel.yaml`.
|
||||
|
||||
## Запуск
|
||||
|
||||
```bash
|
||||
cd demo-backend
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
uvicorn app.main:app --reload --port 8010
|
||||
```
|
||||
|
||||
## Запуск в Docker
|
||||
|
||||
```bash
|
||||
cd demo-backend
|
||||
docker network create shop-network 2>/dev/null || true
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
Остановка:
|
||||
|
||||
```bash
|
||||
docker compose down
|
||||
```
|
||||
|
||||
## Что реализовано
|
||||
|
||||
Travel линейный сценарий:
|
||||
- `GET /users/recent` (`operationId: getRecentUsers`)
|
||||
- `GET /hotels/top` (`operationId: getTopHotels`)
|
||||
- `POST /segments/hotel` (`operationId: segmentUsersByHotelPreferences`)
|
||||
- `POST /assignments/hotels` (`operationId: assignUsersToHotels`)
|
||||
- `POST /emails/send-offers` (`operationId: sendHotelOffersByEmail`)
|
||||
|
||||
CRM линейный сценарий:
|
||||
- `GET /crm/leads/recent` (`operationId: getRecentLeads`)
|
||||
- `POST /crm/leads/qualify` (`operationId: qualifyLeadsForOffer`)
|
||||
- `POST /crm/offers/prepare` (`operationId: prepareOffersForLeads`)
|
||||
- `POST /crm/offers/send` (`operationId: sendPreparedOffers`)
|
||||
|
||||
Swagger UI: `http://localhost:8010/docs`
|
||||
OpenAPI JSON: `http://localhost:8010/openapi.json`
|
||||
|
||||
Для генерации/запуска pipeline в основном backend импортируй именно
|
||||
`demo-backend/openapi/travel.yaml`:
|
||||
- `servers[0].url` = `http://demo-api:8010` (работает для backend-контейнера в `shop-network`)
|
||||
- `servers[1].url` = `http://localhost:8010` (локальный запуск без Docker)
|
||||
- у `template_id` задан `default`, чтобы one-click execution не требовал ручной ввод
|
||||
|
||||
Для CRM-сценария используй `demo-backend/openapi/crm_linear_pipeline.yaml`.
|
||||
|
||||
Если хочешь загрузить сразу все демо-ручки одним файлом:
|
||||
`demo-backend/openapi/all_linear_scenarios.yaml`.
|
||||
|
||||
## Быстрая проверка пайплайна
|
||||
|
||||
```bash
|
||||
BASE=http://localhost:8010
|
||||
|
||||
curl -s "$BASE/users/recent?limit=3" > /tmp/users.json
|
||||
curl -s "$BASE/hotels/top?limit=2" > /tmp/hotels.json
|
||||
|
||||
jq -n \
|
||||
--argjson users "$(jq '.users' /tmp/users.json)" \
|
||||
--argjson hotels "$(jq '.hotels' /tmp/hotels.json)" \
|
||||
'{users:$users, hotels:$hotels}' \
|
||||
| curl -s -X POST "$BASE/segments/hotel" \
|
||||
-H 'content-type: application/json' -d @- > /tmp/segments.json
|
||||
|
||||
jq -n --argjson segments "$(jq '.segments' /tmp/segments.json)" '{segments:$segments}' \
|
||||
| curl -s -X POST "$BASE/assignments/hotels" \
|
||||
-H 'content-type: application/json' -d @- > /tmp/assignments.json
|
||||
|
||||
jq -n \
|
||||
--arg template_id "offer_template_2026" \
|
||||
--argjson assignments "$(jq '.assignments' /tmp/assignments.json)" \
|
||||
'{template_id:$template_id, assignments:$assignments}' \
|
||||
| curl -s -X POST "$BASE/emails/send-offers" \
|
||||
-H 'content-type: application/json' -d @-
|
||||
```
|
||||
@@ -0,0 +1,390 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import FastAPI, Query
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
id: str
|
||||
email: str
|
||||
last_active: datetime
|
||||
|
||||
|
||||
class Hotel(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
city: str
|
||||
|
||||
|
||||
class Segment(BaseModel):
|
||||
segment_id: str
|
||||
hotel_id: str
|
||||
user_ids: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class Assignment(BaseModel):
|
||||
user_id: str
|
||||
hotel_id: str
|
||||
|
||||
|
||||
class RecentUsersResponse(BaseModel):
|
||||
users: list[User] = Field(default_factory=list)
|
||||
|
||||
|
||||
class TopHotelsResponse(BaseModel):
|
||||
hotels: list[Hotel] = Field(default_factory=list)
|
||||
|
||||
|
||||
class HotelSegmentsRequest(BaseModel):
|
||||
users: list[User] = Field(default_factory=list)
|
||||
hotels: list[Hotel] = Field(default_factory=list)
|
||||
|
||||
|
||||
class HotelSegmentsResponse(BaseModel):
|
||||
segments: list[Segment] = Field(default_factory=list)
|
||||
|
||||
|
||||
class AssignmentsRequest(BaseModel):
|
||||
segments: list[Segment] = Field(default_factory=list)
|
||||
|
||||
|
||||
class AssignmentsResponse(BaseModel):
|
||||
assignments: list[Assignment] = Field(default_factory=list)
|
||||
|
||||
|
||||
class EmailOfferRequest(BaseModel):
|
||||
template_id: str = "offer_template_2026"
|
||||
assignments: list[Assignment] = Field(default_factory=list)
|
||||
|
||||
|
||||
class FailedDelivery(BaseModel):
|
||||
user_id: str
|
||||
reason: str
|
||||
|
||||
|
||||
class EmailOfferResponse(BaseModel):
|
||||
sent_count: int
|
||||
failed_count: int
|
||||
failed: list[FailedDelivery] = Field(default_factory=list)
|
||||
|
||||
|
||||
class Lead(BaseModel):
|
||||
lead_id: str
|
||||
email: str
|
||||
source: str
|
||||
|
||||
|
||||
class QualifiedLead(BaseModel):
|
||||
lead_id: str
|
||||
email: str
|
||||
score: int
|
||||
tier: str
|
||||
|
||||
|
||||
class PreparedOffer(BaseModel):
|
||||
offer_id: str
|
||||
lead_id: str
|
||||
channel: str
|
||||
message: str
|
||||
|
||||
|
||||
class RecentLeadsResponse(BaseModel):
|
||||
leads: list[Lead] = Field(default_factory=list)
|
||||
|
||||
|
||||
class QualifyLeadsRequest(BaseModel):
|
||||
leads: list[Lead] = Field(default_factory=list)
|
||||
|
||||
|
||||
class QualifyLeadsResponse(BaseModel):
|
||||
qualified_leads: list[QualifiedLead] = Field(default_factory=list)
|
||||
|
||||
|
||||
class PrepareOffersRequest(BaseModel):
|
||||
qualified_leads: list[QualifiedLead] = Field(default_factory=list)
|
||||
|
||||
|
||||
class PrepareOffersResponse(BaseModel):
|
||||
offers: list[PreparedOffer] = Field(default_factory=list)
|
||||
|
||||
|
||||
class SendOffersRequest(BaseModel):
|
||||
offers: list[PreparedOffer] = Field(default_factory=list)
|
||||
|
||||
|
||||
class FailedLeadDelivery(BaseModel):
|
||||
lead_id: str
|
||||
reason: str
|
||||
|
||||
|
||||
class SendOffersResponse(BaseModel):
|
||||
sent_count: int
|
||||
failed_count: int
|
||||
failed: list[FailedLeadDelivery] = Field(default_factory=list)
|
||||
|
||||
|
||||
APP_DESCRIPTION = """
|
||||
Synthetic API with multiple linear demo workflows.
|
||||
|
||||
Travel workflow:
|
||||
1. `GET /users/recent`
|
||||
2. `GET /hotels/top`
|
||||
3. `POST /segments/hotel`
|
||||
4. `POST /assignments/hotels`
|
||||
5. `POST /emails/send-offers`
|
||||
|
||||
CRM workflow:
|
||||
1. `GET /crm/leads/recent`
|
||||
2. `POST /crm/leads/qualify`
|
||||
3. `POST /crm/offers/prepare`
|
||||
4. `POST /crm/offers/send`
|
||||
""".strip()
|
||||
|
||||
|
||||
app = FastAPI(
|
||||
title="Travel Product Manager API",
|
||||
version="1.0.0",
|
||||
description=APP_DESCRIPTION,
|
||||
)
|
||||
|
||||
|
||||
BASE_USERS_TS = datetime(2026, 3, 13, 10, 0, tzinfo=timezone.utc)
|
||||
HOTEL_CATALOG: list[Hotel] = [
|
||||
Hotel(id="hotel_001", name="Hotel Aurora", city="Berlin"),
|
||||
Hotel(id="hotel_002", name="Sea Breeze Resort", city="Lisbon"),
|
||||
Hotel(id="hotel_003", name="Mountain Vista", city="Zurich"),
|
||||
Hotel(id="hotel_004", name="City Loft", city="Amsterdam"),
|
||||
Hotel(id="hotel_005", name="River Palace", city="Prague"),
|
||||
Hotel(id="hotel_006", name="Nordic Harbor", city="Stockholm"),
|
||||
Hotel(id="hotel_007", name="Sunset Bay", city="Barcelona"),
|
||||
Hotel(id="hotel_008", name="Alpine Crown", city="Vienna"),
|
||||
]
|
||||
|
||||
|
||||
def _build_users() -> list[User]:
|
||||
users: list[User] = []
|
||||
for idx in range(1, 31):
|
||||
users.append(
|
||||
User(
|
||||
id=f"usr_{idx:03d}",
|
||||
email=f"user{idx:03d}@example.com",
|
||||
last_active=BASE_USERS_TS - timedelta(minutes=(idx - 1) * 5),
|
||||
)
|
||||
)
|
||||
return users
|
||||
|
||||
|
||||
def _build_recent_leads() -> list[Lead]:
|
||||
leads: list[Lead] = []
|
||||
sources = ["landing", "webinar", "partner", "organic"]
|
||||
for idx in range(1, 21):
|
||||
leads.append(
|
||||
Lead(
|
||||
lead_id=f"lead_{idx:03d}",
|
||||
email=f"lead{idx:03d}@example.com",
|
||||
source=sources[idx % len(sources)],
|
||||
)
|
||||
)
|
||||
return leads
|
||||
|
||||
|
||||
@app.get(
|
||||
"/users/recent",
|
||||
response_model=RecentUsersResponse,
|
||||
operation_id="getRecentUsers",
|
||||
tags=["travel-offer-workflow"],
|
||||
)
|
||||
async def get_recent_users(
|
||||
last_active_after: Annotated[datetime | None, Query()] = None,
|
||||
limit: Annotated[int, Query(ge=1, le=100)] = 30,
|
||||
) -> RecentUsersResponse:
|
||||
users = _build_users()
|
||||
if last_active_after is not None:
|
||||
users = [user for user in users if user.last_active > last_active_after]
|
||||
return RecentUsersResponse(users=users[:limit])
|
||||
|
||||
|
||||
@app.get(
|
||||
"/hotels/top",
|
||||
response_model=TopHotelsResponse,
|
||||
operation_id="getTopHotels",
|
||||
tags=["travel-offer-workflow"],
|
||||
)
|
||||
async def get_top_hotels(
|
||||
limit: Annotated[int, Query(ge=1, le=20)] = 5,
|
||||
city: Annotated[str | None, Query()] = None,
|
||||
) -> TopHotelsResponse:
|
||||
hotels = HOTEL_CATALOG
|
||||
if city:
|
||||
city_normalized = city.strip().lower()
|
||||
hotels = [hotel for hotel in hotels if hotel.city.lower() == city_normalized]
|
||||
return TopHotelsResponse(hotels=hotels[:limit])
|
||||
|
||||
|
||||
@app.post(
|
||||
"/segments/hotel",
|
||||
response_model=HotelSegmentsResponse,
|
||||
operation_id="segmentUsersByHotelPreferences",
|
||||
tags=["travel-offer-workflow"],
|
||||
)
|
||||
async def segment_users_by_hotel_preferences(
|
||||
payload: HotelSegmentsRequest,
|
||||
) -> HotelSegmentsResponse:
|
||||
if not payload.users or not payload.hotels:
|
||||
return HotelSegmentsResponse(segments=[])
|
||||
|
||||
grouped: dict[str, list[str]] = {hotel.id: [] for hotel in payload.hotels}
|
||||
for index, user in enumerate(payload.users):
|
||||
hotel = payload.hotels[index % len(payload.hotels)]
|
||||
grouped[hotel.id].append(user.id)
|
||||
|
||||
segments: list[Segment] = []
|
||||
for hotel in payload.hotels:
|
||||
user_ids = grouped.get(hotel.id, [])
|
||||
if not user_ids:
|
||||
continue
|
||||
segments.append(
|
||||
Segment(
|
||||
segment_id=f"seg_{hotel.id}",
|
||||
hotel_id=hotel.id,
|
||||
user_ids=user_ids,
|
||||
)
|
||||
)
|
||||
|
||||
return HotelSegmentsResponse(segments=segments)
|
||||
|
||||
|
||||
@app.post(
|
||||
"/assignments/hotels",
|
||||
response_model=AssignmentsResponse,
|
||||
operation_id="assignUsersToHotels",
|
||||
tags=["travel-offer-workflow"],
|
||||
)
|
||||
async def assign_users_to_hotels(payload: AssignmentsRequest) -> AssignmentsResponse:
|
||||
assignments: list[Assignment] = []
|
||||
for segment in payload.segments:
|
||||
for user_id in segment.user_ids:
|
||||
assignments.append(Assignment(user_id=user_id, hotel_id=segment.hotel_id))
|
||||
return AssignmentsResponse(assignments=assignments)
|
||||
|
||||
|
||||
@app.post(
|
||||
"/emails/send-offers",
|
||||
response_model=EmailOfferResponse,
|
||||
status_code=200,
|
||||
operation_id="sendHotelOffersByEmail",
|
||||
tags=["travel-offer-workflow"],
|
||||
|
||||
)
|
||||
async def send_hotel_offers_by_email(payload: EmailOfferRequest) -> EmailOfferResponse:
|
||||
_ = payload.template_id
|
||||
|
||||
failed: list[FailedDelivery] = []
|
||||
for assignment in payload.assignments:
|
||||
if assignment.user_id.endswith("000"):
|
||||
failed.append(
|
||||
FailedDelivery(
|
||||
user_id=assignment.user_id,
|
||||
reason="Invalid user id for delivery",
|
||||
)
|
||||
)
|
||||
|
||||
sent_count = len(payload.assignments) - len(failed)
|
||||
return EmailOfferResponse(
|
||||
sent_count=sent_count,
|
||||
failed_count=len(failed),
|
||||
failed=failed,
|
||||
)
|
||||
|
||||
|
||||
@app.get(
|
||||
"/crm/leads/recent",
|
||||
response_model=RecentLeadsResponse,
|
||||
operation_id="getRecentLeads",
|
||||
tags=["crm-linear-workflow"],
|
||||
)
|
||||
async def get_recent_leads(
|
||||
limit: Annotated[int, Query(ge=1, le=50)] = 20,
|
||||
source: Annotated[str | None, Query()] = None,
|
||||
) -> RecentLeadsResponse:
|
||||
leads = _build_recent_leads()
|
||||
if source:
|
||||
source_normalized = source.strip().lower()
|
||||
leads = [lead for lead in leads if lead.source.lower() == source_normalized]
|
||||
return RecentLeadsResponse(leads=leads[:limit])
|
||||
|
||||
|
||||
@app.post(
|
||||
"/crm/leads/qualify",
|
||||
response_model=QualifyLeadsResponse,
|
||||
operation_id="qualifyLeadsForOffer",
|
||||
tags=["crm-linear-workflow"],
|
||||
)
|
||||
async def qualify_leads_for_offer(payload: QualifyLeadsRequest) -> QualifyLeadsResponse:
|
||||
qualified: list[QualifiedLead] = []
|
||||
for index, lead in enumerate(payload.leads):
|
||||
score = 55 + ((index * 7) % 45)
|
||||
tier = "high" if score >= 80 else "medium" if score >= 65 else "low"
|
||||
qualified.append(
|
||||
QualifiedLead(
|
||||
lead_id=lead.lead_id,
|
||||
email=lead.email,
|
||||
score=score,
|
||||
tier=tier,
|
||||
)
|
||||
)
|
||||
return QualifyLeadsResponse(qualified_leads=qualified)
|
||||
|
||||
|
||||
@app.post(
|
||||
"/crm/offers/prepare",
|
||||
response_model=PrepareOffersResponse,
|
||||
operation_id="prepareOffersForLeads",
|
||||
tags=["crm-linear-workflow"],
|
||||
)
|
||||
async def prepare_offers_for_leads(payload: PrepareOffersRequest) -> PrepareOffersResponse:
|
||||
offers: list[PreparedOffer] = []
|
||||
for lead in payload.qualified_leads:
|
||||
channel = "email" if lead.tier in {"high", "medium"} else "push"
|
||||
offers.append(
|
||||
PreparedOffer(
|
||||
offer_id=f"offer_{lead.lead_id}",
|
||||
lead_id=lead.lead_id,
|
||||
channel=channel,
|
||||
message=f"Special travel offer for {lead.tier} intent lead",
|
||||
)
|
||||
)
|
||||
return PrepareOffersResponse(offers=offers)
|
||||
|
||||
|
||||
@app.post(
|
||||
"/crm/offers/send",
|
||||
response_model=SendOffersResponse,
|
||||
operation_id="sendPreparedOffers",
|
||||
tags=["crm-linear-workflow"],
|
||||
)
|
||||
async def send_prepared_offers(payload: SendOffersRequest) -> SendOffersResponse:
|
||||
failed: list[FailedLeadDelivery] = []
|
||||
for offer in payload.offers:
|
||||
if offer.lead_id.endswith("000"):
|
||||
failed.append(
|
||||
FailedLeadDelivery(
|
||||
lead_id=offer.lead_id,
|
||||
reason="Invalid lead for delivery",
|
||||
)
|
||||
)
|
||||
|
||||
sent_count = len(payload.offers) - len(failed)
|
||||
return SendOffersResponse(
|
||||
sent_count=sent_count,
|
||||
failed_count=len(failed),
|
||||
failed=failed,
|
||||
)
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health() -> dict[str, str]:
|
||||
return {"status": "ok"}
|
||||
@@ -0,0 +1,26 @@
|
||||
services:
|
||||
demo-api:
|
||||
image: ${DOCKER_IMAGE:-demo-backend-api}:${TAG:-latest}
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
restart: always
|
||||
ports:
|
||||
- "8010:8010"
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD",
|
||||
"python",
|
||||
"-c",
|
||||
"import urllib.request; urllib.request.urlopen('http://localhost:8010/health').read()",
|
||||
]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
networks:
|
||||
- shop-network
|
||||
|
||||
networks:
|
||||
shop-network:
|
||||
external: true
|
||||
@@ -0,0 +1,658 @@
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: Travel Product Manager API
|
||||
description: 'Synthetic API with multiple linear demo workflows.
|
||||
|
||||
|
||||
Travel workflow:
|
||||
|
||||
1. `GET /users/recent`
|
||||
|
||||
2. `GET /hotels/top`
|
||||
|
||||
3. `POST /segments/hotel`
|
||||
|
||||
4. `POST /assignments/hotels`
|
||||
|
||||
5. `POST /emails/send-offers`
|
||||
|
||||
|
||||
CRM workflow:
|
||||
|
||||
1. `GET /crm/leads/recent`
|
||||
|
||||
2. `POST /crm/leads/qualify`
|
||||
|
||||
3. `POST /crm/offers/prepare`
|
||||
|
||||
4. `POST /crm/offers/send`'
|
||||
version: 1.0.0
|
||||
servers:
|
||||
- url: http://84.201.161.175
|
||||
description: production
|
||||
paths:
|
||||
/users/recent:
|
||||
get:
|
||||
tags:
|
||||
- travel-offer-workflow
|
||||
summary: Get Recent Users
|
||||
operationId: getRecentUsers
|
||||
parameters:
|
||||
- name: last_active_after
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
anyOf:
|
||||
- type: string
|
||||
format: date-time
|
||||
- type: 'null'
|
||||
title: Last Active After
|
||||
- name: limit
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
maximum: 100
|
||||
minimum: 1
|
||||
default: 30
|
||||
title: Limit
|
||||
responses:
|
||||
'200':
|
||||
description: Successful Response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RecentUsersResponse'
|
||||
'422':
|
||||
description: Validation Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/HTTPValidationError'
|
||||
/hotels/top:
|
||||
get:
|
||||
tags:
|
||||
- travel-offer-workflow
|
||||
summary: Get Top Hotels
|
||||
operationId: getTopHotels
|
||||
parameters:
|
||||
- name: limit
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
maximum: 20
|
||||
minimum: 1
|
||||
default: 5
|
||||
title: Limit
|
||||
- name: city
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
anyOf:
|
||||
- type: string
|
||||
- type: 'null'
|
||||
title: City
|
||||
responses:
|
||||
'200':
|
||||
description: Successful Response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/TopHotelsResponse'
|
||||
'422':
|
||||
description: Validation Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/HTTPValidationError'
|
||||
/segments/hotel:
|
||||
post:
|
||||
tags:
|
||||
- travel-offer-workflow
|
||||
summary: Segment Users By Hotel Preferences
|
||||
operationId: segmentUsersByHotelPreferences
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/HotelSegmentsRequest'
|
||||
required: true
|
||||
responses:
|
||||
'200':
|
||||
description: Successful Response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/HotelSegmentsResponse'
|
||||
'422':
|
||||
description: Validation Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/HTTPValidationError'
|
||||
/assignments/hotels:
|
||||
post:
|
||||
tags:
|
||||
- travel-offer-workflow
|
||||
summary: Assign Users To Hotels
|
||||
operationId: assignUsersToHotels
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AssignmentsRequest'
|
||||
required: true
|
||||
responses:
|
||||
'200':
|
||||
description: Successful Response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AssignmentsResponse'
|
||||
'422':
|
||||
description: Validation Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/HTTPValidationError'
|
||||
/emails/send-offers:
|
||||
post:
|
||||
tags:
|
||||
- travel-offer-workflow
|
||||
summary: Send Hotel Offers By Email
|
||||
operationId: sendHotelOffersByEmail
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/EmailOfferRequest'
|
||||
required: true
|
||||
responses:
|
||||
'200':
|
||||
description: Successful Response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/EmailOfferResponse'
|
||||
'422':
|
||||
description: Validation Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/HTTPValidationError'
|
||||
/crm/leads/recent:
|
||||
get:
|
||||
tags:
|
||||
- crm-linear-workflow
|
||||
summary: Get Recent Leads
|
||||
operationId: getRecentLeads
|
||||
parameters:
|
||||
- name: limit
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
maximum: 50
|
||||
minimum: 1
|
||||
default: 20
|
||||
title: Limit
|
||||
- name: source
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
anyOf:
|
||||
- type: string
|
||||
- type: 'null'
|
||||
title: Source
|
||||
responses:
|
||||
'200':
|
||||
description: Successful Response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RecentLeadsResponse'
|
||||
'422':
|
||||
description: Validation Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/HTTPValidationError'
|
||||
/crm/leads/qualify:
|
||||
post:
|
||||
tags:
|
||||
- crm-linear-workflow
|
||||
summary: Qualify Leads For Offer
|
||||
operationId: qualifyLeadsForOffer
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/QualifyLeadsRequest'
|
||||
required: true
|
||||
responses:
|
||||
'200':
|
||||
description: Successful Response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/QualifyLeadsResponse'
|
||||
'422':
|
||||
description: Validation Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/HTTPValidationError'
|
||||
/crm/offers/prepare:
|
||||
post:
|
||||
tags:
|
||||
- crm-linear-workflow
|
||||
summary: Prepare Offers For Leads
|
||||
operationId: prepareOffersForLeads
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PrepareOffersRequest'
|
||||
required: true
|
||||
responses:
|
||||
'200':
|
||||
description: Successful Response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PrepareOffersResponse'
|
||||
'422':
|
||||
description: Validation Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/HTTPValidationError'
|
||||
/crm/offers/send:
|
||||
post:
|
||||
tags:
|
||||
- crm-linear-workflow
|
||||
summary: Send Prepared Offers
|
||||
operationId: sendPreparedOffers
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SendOffersRequest'
|
||||
required: true
|
||||
responses:
|
||||
'200':
|
||||
description: Successful Response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SendOffersResponse'
|
||||
'422':
|
||||
description: Validation Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/HTTPValidationError'
|
||||
/health:
|
||||
get:
|
||||
summary: Health
|
||||
operationId: health_health_get
|
||||
responses:
|
||||
'200':
|
||||
description: Successful Response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
title: Response Health Health Get
|
||||
components:
|
||||
schemas:
|
||||
Assignment:
|
||||
properties:
|
||||
user_id:
|
||||
type: string
|
||||
title: User Id
|
||||
hotel_id:
|
||||
type: string
|
||||
title: Hotel Id
|
||||
type: object
|
||||
required:
|
||||
- user_id
|
||||
- hotel_id
|
||||
title: Assignment
|
||||
AssignmentsRequest:
|
||||
properties:
|
||||
segments:
|
||||
items:
|
||||
$ref: '#/components/schemas/Segment'
|
||||
type: array
|
||||
title: Segments
|
||||
type: object
|
||||
title: AssignmentsRequest
|
||||
AssignmentsResponse:
|
||||
properties:
|
||||
assignments:
|
||||
items:
|
||||
$ref: '#/components/schemas/Assignment'
|
||||
type: array
|
||||
title: Assignments
|
||||
type: object
|
||||
title: AssignmentsResponse
|
||||
EmailOfferRequest:
|
||||
properties:
|
||||
template_id:
|
||||
type: string
|
||||
title: Template Id
|
||||
assignments:
|
||||
items:
|
||||
$ref: '#/components/schemas/Assignment'
|
||||
type: array
|
||||
title: Assignments
|
||||
type: object
|
||||
required:
|
||||
- template_id
|
||||
title: EmailOfferRequest
|
||||
EmailOfferResponse:
|
||||
properties:
|
||||
sent_count:
|
||||
type: integer
|
||||
title: Sent Count
|
||||
failed_count:
|
||||
type: integer
|
||||
title: Failed Count
|
||||
failed:
|
||||
items:
|
||||
$ref: '#/components/schemas/FailedDelivery'
|
||||
type: array
|
||||
title: Failed
|
||||
type: object
|
||||
required:
|
||||
- sent_count
|
||||
- failed_count
|
||||
title: EmailOfferResponse
|
||||
FailedDelivery:
|
||||
properties:
|
||||
user_id:
|
||||
type: string
|
||||
title: User Id
|
||||
reason:
|
||||
type: string
|
||||
title: Reason
|
||||
type: object
|
||||
required:
|
||||
- user_id
|
||||
- reason
|
||||
title: FailedDelivery
|
||||
FailedLeadDelivery:
|
||||
properties:
|
||||
lead_id:
|
||||
type: string
|
||||
title: Lead Id
|
||||
reason:
|
||||
type: string
|
||||
title: Reason
|
||||
type: object
|
||||
required:
|
||||
- lead_id
|
||||
- reason
|
||||
title: FailedLeadDelivery
|
||||
HTTPValidationError:
|
||||
properties:
|
||||
detail:
|
||||
items:
|
||||
$ref: '#/components/schemas/ValidationError'
|
||||
type: array
|
||||
title: Detail
|
||||
type: object
|
||||
title: HTTPValidationError
|
||||
Hotel:
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
title: Id
|
||||
name:
|
||||
type: string
|
||||
title: Name
|
||||
city:
|
||||
type: string
|
||||
title: City
|
||||
type: object
|
||||
required:
|
||||
- id
|
||||
- name
|
||||
- city
|
||||
title: Hotel
|
||||
HotelSegmentsRequest:
|
||||
properties:
|
||||
users:
|
||||
items:
|
||||
$ref: '#/components/schemas/User'
|
||||
type: array
|
||||
title: Users
|
||||
hotels:
|
||||
items:
|
||||
$ref: '#/components/schemas/Hotel'
|
||||
type: array
|
||||
title: Hotels
|
||||
type: object
|
||||
title: HotelSegmentsRequest
|
||||
HotelSegmentsResponse:
|
||||
properties:
|
||||
segments:
|
||||
items:
|
||||
$ref: '#/components/schemas/Segment'
|
||||
type: array
|
||||
title: Segments
|
||||
type: object
|
||||
title: HotelSegmentsResponse
|
||||
Lead:
|
||||
properties:
|
||||
lead_id:
|
||||
type: string
|
||||
title: Lead Id
|
||||
email:
|
||||
type: string
|
||||
title: Email
|
||||
source:
|
||||
type: string
|
||||
title: Source
|
||||
type: object
|
||||
required:
|
||||
- lead_id
|
||||
- email
|
||||
- source
|
||||
title: Lead
|
||||
PrepareOffersRequest:
|
||||
properties:
|
||||
qualified_leads:
|
||||
items:
|
||||
$ref: '#/components/schemas/QualifiedLead'
|
||||
type: array
|
||||
title: Qualified Leads
|
||||
type: object
|
||||
title: PrepareOffersRequest
|
||||
PrepareOffersResponse:
|
||||
properties:
|
||||
offers:
|
||||
items:
|
||||
$ref: '#/components/schemas/PreparedOffer'
|
||||
type: array
|
||||
title: Offers
|
||||
type: object
|
||||
title: PrepareOffersResponse
|
||||
PreparedOffer:
|
||||
properties:
|
||||
offer_id:
|
||||
type: string
|
||||
title: Offer Id
|
||||
lead_id:
|
||||
type: string
|
||||
title: Lead Id
|
||||
channel:
|
||||
type: string
|
||||
title: Channel
|
||||
message:
|
||||
type: string
|
||||
title: Message
|
||||
type: object
|
||||
required:
|
||||
- offer_id
|
||||
- lead_id
|
||||
- channel
|
||||
- message
|
||||
title: PreparedOffer
|
||||
QualifiedLead:
|
||||
properties:
|
||||
lead_id:
|
||||
type: string
|
||||
title: Lead Id
|
||||
email:
|
||||
type: string
|
||||
title: Email
|
||||
score:
|
||||
type: integer
|
||||
title: Score
|
||||
tier:
|
||||
type: string
|
||||
title: Tier
|
||||
type: object
|
||||
required:
|
||||
- lead_id
|
||||
- email
|
||||
- score
|
||||
- tier
|
||||
title: QualifiedLead
|
||||
QualifyLeadsRequest:
|
||||
properties:
|
||||
leads:
|
||||
items:
|
||||
$ref: '#/components/schemas/Lead'
|
||||
type: array
|
||||
title: Leads
|
||||
type: object
|
||||
title: QualifyLeadsRequest
|
||||
QualifyLeadsResponse:
|
||||
properties:
|
||||
qualified_leads:
|
||||
items:
|
||||
$ref: '#/components/schemas/QualifiedLead'
|
||||
type: array
|
||||
title: Qualified Leads
|
||||
type: object
|
||||
title: QualifyLeadsResponse
|
||||
RecentLeadsResponse:
|
||||
properties:
|
||||
leads:
|
||||
items:
|
||||
$ref: '#/components/schemas/Lead'
|
||||
type: array
|
||||
title: Leads
|
||||
type: object
|
||||
title: RecentLeadsResponse
|
||||
RecentUsersResponse:
|
||||
properties:
|
||||
users:
|
||||
items:
|
||||
$ref: '#/components/schemas/User'
|
||||
type: array
|
||||
title: Users
|
||||
type: object
|
||||
title: RecentUsersResponse
|
||||
Segment:
|
||||
properties:
|
||||
segment_id:
|
||||
type: string
|
||||
title: Segment Id
|
||||
hotel_id:
|
||||
type: string
|
||||
title: Hotel Id
|
||||
user_ids:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
title: User Ids
|
||||
type: object
|
||||
required:
|
||||
- segment_id
|
||||
- hotel_id
|
||||
title: Segment
|
||||
SendOffersRequest:
|
||||
properties:
|
||||
offers:
|
||||
items:
|
||||
$ref: '#/components/schemas/PreparedOffer'
|
||||
type: array
|
||||
title: Offers
|
||||
type: object
|
||||
title: SendOffersRequest
|
||||
SendOffersResponse:
|
||||
properties:
|
||||
sent_count:
|
||||
type: integer
|
||||
title: Sent Count
|
||||
failed_count:
|
||||
type: integer
|
||||
title: Failed Count
|
||||
failed:
|
||||
items:
|
||||
$ref: '#/components/schemas/FailedLeadDelivery'
|
||||
type: array
|
||||
title: Failed
|
||||
type: object
|
||||
required:
|
||||
- sent_count
|
||||
- failed_count
|
||||
title: SendOffersResponse
|
||||
TopHotelsResponse:
|
||||
properties:
|
||||
hotels:
|
||||
items:
|
||||
$ref: '#/components/schemas/Hotel'
|
||||
type: array
|
||||
title: Hotels
|
||||
type: object
|
||||
title: TopHotelsResponse
|
||||
User:
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
title: Id
|
||||
email:
|
||||
type: string
|
||||
title: Email
|
||||
last_active:
|
||||
type: string
|
||||
format: date-time
|
||||
title: Last Active
|
||||
type: object
|
||||
required:
|
||||
- id
|
||||
- email
|
||||
- last_active
|
||||
title: User
|
||||
ValidationError:
|
||||
properties:
|
||||
loc:
|
||||
items:
|
||||
anyOf:
|
||||
- type: string
|
||||
- type: integer
|
||||
type: array
|
||||
title: Location
|
||||
msg:
|
||||
type: string
|
||||
title: Message
|
||||
type:
|
||||
type: string
|
||||
title: Error Type
|
||||
type: object
|
||||
required:
|
||||
- loc
|
||||
- msg
|
||||
- type
|
||||
title: ValidationError
|
||||
servers:
|
||||
- url: http://demo-api:8010
|
||||
- url: http://localhost:8010
|
||||
@@ -0,0 +1,214 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: CRM Linear Demo API
|
||||
version: 1.0.0
|
||||
description: |
|
||||
Demo OpenAPI for a strict linear CRM scenario:
|
||||
1) get recent leads,
|
||||
2) qualify leads,
|
||||
3) prepare offers,
|
||||
4) send offers.
|
||||
servers:
|
||||
- url: http://demo-api:8010
|
||||
- url: http://localhost:8010
|
||||
paths:
|
||||
/crm/leads/recent:
|
||||
get:
|
||||
operationId: getRecentLeads
|
||||
tags: [crm-linear-workflow]
|
||||
summary: Get recent leads
|
||||
parameters:
|
||||
- in: query
|
||||
name: limit
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 50
|
||||
default: 20
|
||||
- in: query
|
||||
name: source
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: Leads list
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/RecentLeadsResponse"
|
||||
|
||||
/crm/leads/qualify:
|
||||
post:
|
||||
operationId: qualifyLeadsForOffer
|
||||
tags: [crm-linear-workflow]
|
||||
summary: Qualify leads
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/QualifyLeadsRequest"
|
||||
responses:
|
||||
"200":
|
||||
description: Qualified leads
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/QualifyLeadsResponse"
|
||||
|
||||
/crm/offers/prepare:
|
||||
post:
|
||||
operationId: prepareOffersForLeads
|
||||
tags: [crm-linear-workflow]
|
||||
summary: Prepare offers from qualified leads
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/PrepareOffersRequest"
|
||||
responses:
|
||||
"200":
|
||||
description: Prepared offers
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/PrepareOffersResponse"
|
||||
|
||||
/crm/offers/send:
|
||||
post:
|
||||
operationId: sendPreparedOffers
|
||||
tags: [crm-linear-workflow]
|
||||
summary: Send prepared offers
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/SendOffersRequest"
|
||||
responses:
|
||||
"200":
|
||||
description: Send summary
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/SendOffersResponse"
|
||||
|
||||
components:
|
||||
schemas:
|
||||
Lead:
|
||||
type: object
|
||||
required: [lead_id, email, source]
|
||||
properties:
|
||||
lead_id:
|
||||
type: string
|
||||
email:
|
||||
type: string
|
||||
format: email
|
||||
source:
|
||||
type: string
|
||||
|
||||
QualifiedLead:
|
||||
type: object
|
||||
required: [lead_id, email, score, tier]
|
||||
properties:
|
||||
lead_id:
|
||||
type: string
|
||||
email:
|
||||
type: string
|
||||
format: email
|
||||
score:
|
||||
type: integer
|
||||
tier:
|
||||
type: string
|
||||
|
||||
PreparedOffer:
|
||||
type: object
|
||||
required: [offer_id, lead_id, channel, message]
|
||||
properties:
|
||||
offer_id:
|
||||
type: string
|
||||
lead_id:
|
||||
type: string
|
||||
channel:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
|
||||
RecentLeadsResponse:
|
||||
type: object
|
||||
required: [leads]
|
||||
properties:
|
||||
leads:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/Lead"
|
||||
|
||||
QualifyLeadsRequest:
|
||||
type: object
|
||||
required: [leads]
|
||||
properties:
|
||||
leads:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/Lead"
|
||||
|
||||
QualifyLeadsResponse:
|
||||
type: object
|
||||
required: [qualified_leads]
|
||||
properties:
|
||||
qualified_leads:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/QualifiedLead"
|
||||
|
||||
PrepareOffersRequest:
|
||||
type: object
|
||||
required: [qualified_leads]
|
||||
properties:
|
||||
qualified_leads:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/QualifiedLead"
|
||||
|
||||
PrepareOffersResponse:
|
||||
type: object
|
||||
required: [offers]
|
||||
properties:
|
||||
offers:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/PreparedOffer"
|
||||
|
||||
SendOffersRequest:
|
||||
type: object
|
||||
required: [offers]
|
||||
properties:
|
||||
offers:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/PreparedOffer"
|
||||
|
||||
FailedLeadDelivery:
|
||||
type: object
|
||||
required: [lead_id, reason]
|
||||
properties:
|
||||
lead_id:
|
||||
type: string
|
||||
reason:
|
||||
type: string
|
||||
|
||||
SendOffersResponse:
|
||||
type: object
|
||||
required: [sent_count, failed_count, failed]
|
||||
properties:
|
||||
sent_count:
|
||||
type: integer
|
||||
failed_count:
|
||||
type: integer
|
||||
failed:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/FailedLeadDelivery"
|
||||
@@ -0,0 +1,144 @@
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: Travel & CRM Pipeline API
|
||||
description: |
|
||||
Это API предназначено для автоматизации маркетинговых и операционных процессов в сфере туризма.
|
||||
Оно поддерживает два основных сценария автоматизации (пайплайна):
|
||||
|
||||
### 1. Сценарий: Рассылка спецпредложений по отелям
|
||||
Используется для реактивации пользователей, которые были активны недавно.
|
||||
**Цепочка:** Получение юзеров → Подбор топ-отелей → Сегментация (матчинг) → Назначение конкретных пар Юзер-Отель → Отправка Email.
|
||||
|
||||
### 2. Сценарий: Обработка лидов в CRM
|
||||
Предназначен для отдела продаж.
|
||||
**Цепочка:** Сбор новых лидов → Квалификация (оценка качества) → Подготовка оффера → Финальная отправка.
|
||||
version: 1.1.0
|
||||
servers:
|
||||
- url: http://84.201.161.175
|
||||
paths:
|
||||
/users/recent:
|
||||
get:
|
||||
tags:
|
||||
- travel-offer-workflow
|
||||
summary: Получить список недавно активных пользователей
|
||||
description: |
|
||||
Возвращает список клиентов, которые заходили в приложение за последнее время.
|
||||
Используйте этот метод как входную точку для начала маркетинговой кампании.
|
||||
operationId: getRecentUsers
|
||||
parameters:
|
||||
- name: last_active_after
|
||||
in: query
|
||||
description: Фильтр по дате и времени. Будут возвращены только те, кто был активен ПОСЛЕ указанного момента.
|
||||
required: false
|
||||
schema:
|
||||
anyOf:
|
||||
- type: string
|
||||
format: date-time
|
||||
- type: "null"
|
||||
- name: limit
|
||||
in: query
|
||||
description: Ограничение выборки. По умолчанию возвращается 30 пользователей для оптимальной нагрузки на почтовый сервер.
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
maximum: 100
|
||||
minimum: 1
|
||||
default: 30
|
||||
responses:
|
||||
"200":
|
||||
description: Список пользователей успешно сформирован.
|
||||
|
||||
/hotels/top:
|
||||
get:
|
||||
tags:
|
||||
- travel-offer-workflow
|
||||
summary: Получить список популярных отелей
|
||||
description: |
|
||||
Выгружает наиболее востребованные отели. Можно фильтровать по конкретному городу, чтобы сделать предложение более точным.
|
||||
operationId: getTopHotels
|
||||
parameters:
|
||||
- name: limit
|
||||
in: query
|
||||
description: Максимальное количество отелей в выдаче (не более 20).
|
||||
- name: city
|
||||
in: query
|
||||
description: Название города (например, 'Moscow', 'Dubai'). Если не указано, вернутся топ-отели по всем направлениям.
|
||||
responses:
|
||||
"200":
|
||||
description: Список отелей получен.
|
||||
|
||||
/segments/hotel:
|
||||
post:
|
||||
tags:
|
||||
- travel-offer-workflow
|
||||
summary: Сгруппировать пользователей по интересам к отелям
|
||||
description: |
|
||||
Принимает списки пользователей и отелей, анализирует их и создает группы (сегменты).
|
||||
Это "умный" этап, который определяет, кому какой тип отдыха подходит больше.
|
||||
operationId: segmentUsersByHotelPreferences
|
||||
requestBody:
|
||||
description: Данные для анализа (массивы объектов User и Hotel).
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/HotelSegmentsRequest"
|
||||
responses:
|
||||
"200":
|
||||
description: Сегментация успешно завершена.
|
||||
|
||||
/assignments/hotels:
|
||||
post:
|
||||
tags:
|
||||
- travel-offer-workflow
|
||||
summary: Назначить конкретные отели пользователям
|
||||
description: |
|
||||
Финальное закрепление. На основе сегментов метод создает пары "ID пользователя — ID отеля".
|
||||
Результат этого метода передается напрямую в сервис рассылки.
|
||||
operationId: assignUsersToHotels
|
||||
responses:
|
||||
"200":
|
||||
description: Пары для рассылки сформированы.
|
||||
|
||||
/emails/send-offers:
|
||||
post:
|
||||
tags:
|
||||
- travel-offer-workflow
|
||||
summary: Разослать персонализированные предложения
|
||||
description: |
|
||||
Запускает процесс отправки писем. Требует ID шаблона письма и список назначений, сформированный на предыдущем шаге.
|
||||
operationId: sendHotelOffersByEmail
|
||||
requestBody:
|
||||
description: Шаблон письма и список получателей с назначенными им отелями.
|
||||
responses:
|
||||
"200":
|
||||
description: Рассылка запущена. В ответе придет статистика (сколько отправлено, сколько сбоев).
|
||||
|
||||
/crm/leads/qualify:
|
||||
post:
|
||||
tags:
|
||||
- crm-linear-workflow
|
||||
summary: Оценить качество лидов (Lead Scoring)
|
||||
description: |
|
||||
Метод проверяет входящие заявки и присваивает им рейтинг (score) и уровень (tier).
|
||||
Это позволяет продакту сфокусироваться на самых "горячих" клиентах.
|
||||
operationId: qualifyLeadsForOffer
|
||||
responses:
|
||||
"200":
|
||||
description: Лиды успешно квалифицированы.
|
||||
|
||||
components:
|
||||
schemas:
|
||||
User:
|
||||
type: object
|
||||
description: Информация о клиенте сервиса.
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
description: Уникальный идентификатор пользователя (UUID).
|
||||
email:
|
||||
type: string
|
||||
description: Адрес электронной почты для связи.
|
||||
last_active:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Таймстамп последнего действия в системе
|
||||
@@ -0,0 +1,556 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: Travel Product Manager API
|
||||
version: 1.0.0
|
||||
description: |
|
||||
Synthetic API for a single travel offer workflow.
|
||||
Intended order of operations:
|
||||
1. get recent users,
|
||||
2. get top hotels,
|
||||
3. build hotel preference segments from users and hotels,
|
||||
4. build user-to-hotel assignments from segments,
|
||||
5. send hotel offers by email from assignments.
|
||||
Each endpoint has one specific responsibility.
|
||||
The workflow should be interpreted as a strict data pipeline where the output
|
||||
array of one step becomes the input field of the next step.
|
||||
servers:
|
||||
- url: http://demo-api:8010
|
||||
- url: http://localhost:8010
|
||||
- url: https://api.travel.example.com
|
||||
paths:
|
||||
/users/recent:
|
||||
get:
|
||||
operationId: getRecentUsers
|
||||
tags:
|
||||
- travel-offer-workflow
|
||||
summary: Get recent users for travel campaigns
|
||||
description: |
|
||||
Returns a list of recent users active in the last 7 days.
|
||||
By default this endpoint returns up to 30 users because the limit parameter
|
||||
defaults to 30.
|
||||
Output of this endpoint is the users array that should be passed as the users
|
||||
field to /segments/hotel.
|
||||
This endpoint does not retrieve hotels, create segments, create assignments,
|
||||
or send emails.
|
||||
parameters:
|
||||
- in: query
|
||||
name: last_active_after
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
required: false
|
||||
description: |
|
||||
Optional lower bound for user activity time.
|
||||
Only users active after this timestamp should be returned.
|
||||
If omitted, the endpoint behaves like "last 7 days".
|
||||
- in: query
|
||||
name: limit
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 100
|
||||
default: 30
|
||||
required: false
|
||||
description: |
|
||||
Maximum number of users to return.
|
||||
If omitted, the endpoint returns up to 30 users.
|
||||
responses:
|
||||
"200":
|
||||
description: |
|
||||
Successful response containing the users array for the first workflow step.
|
||||
This users array should be passed forward to /segments/hotel.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/RecentUsersResponse"
|
||||
examples:
|
||||
sample:
|
||||
value:
|
||||
users:
|
||||
- id: usr_001
|
||||
email: user001@example.com
|
||||
last_active: "2026-03-13T10:00:00Z"
|
||||
- id: usr_002
|
||||
email: user002@example.com
|
||||
last_active: "2026-03-13T09:55:00Z"
|
||||
- id: usr_003
|
||||
email: user003@example.com
|
||||
last_active: "2026-03-13T09:50:00Z"
|
||||
- id: usr_004
|
||||
email: user004@example.com
|
||||
last_active: "2026-03-13T09:45:00Z"
|
||||
- id: usr_005
|
||||
email: user005@example.com
|
||||
last_active: "2026-03-13T09:40:00Z"
|
||||
- id: usr_006
|
||||
email: user006@example.com
|
||||
last_active: "2026-03-13T09:35:00Z"
|
||||
- id: usr_007
|
||||
email: user007@example.com
|
||||
last_active: "2026-03-13T09:30:00Z"
|
||||
- id: usr_008
|
||||
email: user008@example.com
|
||||
last_active: "2026-03-13T09:25:00Z"
|
||||
- id: usr_009
|
||||
email: user009@example.com
|
||||
last_active: "2026-03-13T09:20:00Z"
|
||||
- id: usr_010
|
||||
email: user010@example.com
|
||||
last_active: "2026-03-13T09:15:00Z"
|
||||
- id: usr_011
|
||||
email: user011@example.com
|
||||
last_active: "2026-03-13T09:10:00Z"
|
||||
- id: usr_012
|
||||
email: user012@example.com
|
||||
last_active: "2026-03-13T09:05:00Z"
|
||||
- id: usr_013
|
||||
email: user013@example.com
|
||||
last_active: "2026-03-13T09:00:00Z"
|
||||
- id: usr_014
|
||||
email: user014@example.com
|
||||
last_active: "2026-03-13T08:55:00Z"
|
||||
- id: usr_015
|
||||
email: user015@example.com
|
||||
last_active: "2026-03-13T08:50:00Z"
|
||||
- id: usr_016
|
||||
email: user016@example.com
|
||||
last_active: "2026-03-13T08:45:00Z"
|
||||
- id: usr_017
|
||||
email: user017@example.com
|
||||
last_active: "2026-03-13T08:40:00Z"
|
||||
- id: usr_018
|
||||
email: user018@example.com
|
||||
last_active: "2026-03-13T08:35:00Z"
|
||||
- id: usr_019
|
||||
email: user019@example.com
|
||||
last_active: "2026-03-13T08:30:00Z"
|
||||
- id: usr_020
|
||||
email: user020@example.com
|
||||
last_active: "2026-03-13T08:25:00Z"
|
||||
- id: usr_021
|
||||
email: user021@example.com
|
||||
last_active: "2026-03-13T08:20:00Z"
|
||||
- id: usr_022
|
||||
email: user022@example.com
|
||||
last_active: "2026-03-13T08:15:00Z"
|
||||
- id: usr_023
|
||||
email: user023@example.com
|
||||
last_active: "2026-03-13T08:10:00Z"
|
||||
- id: usr_024
|
||||
email: user024@example.com
|
||||
last_active: "2026-03-13T08:05:00Z"
|
||||
- id: usr_025
|
||||
email: user025@example.com
|
||||
last_active: "2026-03-13T08:00:00Z"
|
||||
- id: usr_026
|
||||
email: user026@example.com
|
||||
last_active: "2026-03-13T07:55:00Z"
|
||||
- id: usr_027
|
||||
email: user027@example.com
|
||||
last_active: "2026-03-13T07:50:00Z"
|
||||
- id: usr_028
|
||||
email: user028@example.com
|
||||
last_active: "2026-03-13T07:45:00Z"
|
||||
- id: usr_029
|
||||
email: user029@example.com
|
||||
last_active: "2026-03-13T07:40:00Z"
|
||||
- id: usr_030
|
||||
email: user030@example.com
|
||||
last_active: "2026-03-13T07:35:00Z"
|
||||
/hotels/top:
|
||||
get:
|
||||
operationId: getTopHotels
|
||||
tags:
|
||||
- travel-offer-workflow
|
||||
summary: Get top hotels for offers
|
||||
description: |
|
||||
Returns a list of candidate hotels for the offer workflow.
|
||||
By default this endpoint returns up to 5 hotels because the limit parameter
|
||||
defaults to 5.
|
||||
Output of this endpoint is the hotels array that should be passed as the hotels
|
||||
field to /segments/hotel.
|
||||
This endpoint does not retrieve users, create segments, create assignments,
|
||||
or send emails.
|
||||
parameters:
|
||||
- in: query
|
||||
name: limit
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 20
|
||||
default: 5
|
||||
required: false
|
||||
description: |
|
||||
Maximum number of hotels to return.
|
||||
If omitted, the endpoint returns up to 5 hotels.
|
||||
- in: query
|
||||
name: city
|
||||
schema:
|
||||
type: string
|
||||
required: false
|
||||
description: |
|
||||
Optional city filter.
|
||||
If provided, only hotels from this city should be returned.
|
||||
responses:
|
||||
"200":
|
||||
description: |
|
||||
Successful response containing the hotels array for the second workflow step.
|
||||
This hotels array should be passed forward to /segments/hotel.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/TopHotelsResponse"
|
||||
examples:
|
||||
sample:
|
||||
value:
|
||||
hotels:
|
||||
- id: hotel_001
|
||||
name: Hotel Aurora
|
||||
city: Berlin
|
||||
- id: hotel_002
|
||||
name: Sea Breeze Resort
|
||||
city: Lisbon
|
||||
- id: hotel_003
|
||||
name: Mountain Vista
|
||||
city: Zurich
|
||||
- id: hotel_004
|
||||
name: City Loft
|
||||
city: Amsterdam
|
||||
- id: hotel_005
|
||||
name: River Palace
|
||||
city: Prague
|
||||
/segments/hotel:
|
||||
post:
|
||||
operationId: segmentUsersByHotelPreferences
|
||||
tags:
|
||||
- travel-offer-workflow
|
||||
summary: Segment recent users by hotel preferences
|
||||
description: |
|
||||
Creates hotel-based user segments from two required inputs in one request:
|
||||
users and hotels.
|
||||
The users field must contain the users array returned by /users/recent.
|
||||
The hotels field must contain the hotels array returned by /hotels/top.
|
||||
A common workflow is: get up to 30 recent users, get top hotels, then send
|
||||
both arrays to this endpoint to distribute users across hotels by preference.
|
||||
Output of this endpoint is the segments array used as the segments field in
|
||||
/assignments/hotels.
|
||||
This endpoint does not send emails.
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/HotelSegmentsRequest"
|
||||
examples:
|
||||
sample:
|
||||
value:
|
||||
users:
|
||||
- id: usr_001
|
||||
email: user001@example.com
|
||||
last_active: "2026-03-13T10:00:00Z"
|
||||
hotels:
|
||||
- id: hotel_001
|
||||
name: Hotel Aurora
|
||||
city: Berlin
|
||||
responses:
|
||||
"200":
|
||||
description: |
|
||||
Successful response containing the segments array.
|
||||
This segments array should be passed forward to /assignments/hotels.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/HotelSegmentsResponse"
|
||||
examples:
|
||||
sample:
|
||||
value:
|
||||
segments:
|
||||
- segment_id: seg_berlin
|
||||
hotel_id: hotel_001
|
||||
user_ids: ["usr_001", "usr_002"]
|
||||
/assignments/hotels:
|
||||
post:
|
||||
operationId: assignUsersToHotels
|
||||
tags:
|
||||
- travel-offer-workflow
|
||||
summary: Assign users to hotels based on segments
|
||||
description: |
|
||||
Builds final user-to-hotel assignments from segments.
|
||||
The segments field must contain the segments array returned by /segments/hotel.
|
||||
Output of this endpoint is the assignments array used as the assignments field
|
||||
in /emails/send-offers.
|
||||
This endpoint does not send emails and does not fetch users or hotels.
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/AssignmentsRequest"
|
||||
examples:
|
||||
sample:
|
||||
value:
|
||||
segments:
|
||||
- segment_id: seg_berlin
|
||||
hotel_id: hotel_001
|
||||
user_ids: ["usr_001", "usr_002"]
|
||||
responses:
|
||||
"200":
|
||||
description: |
|
||||
Successful response containing the assignments array.
|
||||
This assignments array should be passed forward to /emails/send-offers.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/AssignmentsResponse"
|
||||
examples:
|
||||
sample:
|
||||
value:
|
||||
assignments:
|
||||
- user_id: usr_001
|
||||
hotel_id: hotel_001
|
||||
- user_id: usr_002
|
||||
hotel_id: hotel_001
|
||||
/emails/send-offers:
|
||||
post:
|
||||
operationId: sendHotelOffersByEmail
|
||||
tags:
|
||||
- travel-offer-workflow
|
||||
summary: Send hotel offers by email
|
||||
description: |
|
||||
Sends hotel offer emails to users based on final assignments.
|
||||
The assignments field must contain the assignments array returned by
|
||||
/assignments/hotels.
|
||||
This endpoint is the final delivery step of the workflow.
|
||||
It does not build new segments or assignments.
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/EmailOfferRequest"
|
||||
examples:
|
||||
sample:
|
||||
value:
|
||||
template_id: offer_template_2026
|
||||
assignments:
|
||||
- user_id: usr_001
|
||||
hotel_id: hotel_001
|
||||
- user_id: usr_002
|
||||
hotel_id: hotel_001
|
||||
responses:
|
||||
"200":
|
||||
description: |
|
||||
Successful response containing the result of the email delivery step.
|
||||
This is the final output of the workflow.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/EmailOfferResponse"
|
||||
examples:
|
||||
sample:
|
||||
value:
|
||||
sent_count: 2
|
||||
failed_count: 0
|
||||
failed: []
|
||||
components:
|
||||
schemas:
|
||||
User:
|
||||
description: |
|
||||
A recent user eligible to receive a hotel offer email.
|
||||
User objects are produced by /users/recent and then reused in
|
||||
/segments/hotel.
|
||||
type: object
|
||||
required: [id, email, last_active]
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
description: Stable unique user identifier.
|
||||
email:
|
||||
type: string
|
||||
format: email
|
||||
description: Email address used in the final offer delivery step.
|
||||
last_active:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Most recent activity timestamp used to identify recent users.
|
||||
Hotel:
|
||||
description: |
|
||||
A hotel candidate that may be recommended to users.
|
||||
Hotel objects are produced by /hotels/top and then reused in
|
||||
/segments/hotel.
|
||||
type: object
|
||||
required: [id, name, city]
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
description: Stable unique hotel identifier.
|
||||
name:
|
||||
type: string
|
||||
description: Human-readable hotel name shown in offers.
|
||||
city:
|
||||
type: string
|
||||
description: City where the hotel is located.
|
||||
Segment:
|
||||
description: |
|
||||
A hotel preference segment that groups users for one hotel.
|
||||
Segment objects are produced by /segments/hotel and then reused in
|
||||
/assignments/hotels.
|
||||
type: object
|
||||
required: [segment_id, hotel_id, user_ids]
|
||||
properties:
|
||||
segment_id:
|
||||
type: string
|
||||
description: Stable unique segment identifier.
|
||||
hotel_id:
|
||||
type: string
|
||||
description: Hotel identifier associated with this segment.
|
||||
user_ids:
|
||||
type: array
|
||||
description: User identifiers that belong to this hotel preference segment.
|
||||
items:
|
||||
type: string
|
||||
Assignment:
|
||||
description: |
|
||||
A final mapping between one user and one hotel offer.
|
||||
Assignment objects are produced by /assignments/hotels and then reused in
|
||||
/emails/send-offers.
|
||||
type: object
|
||||
required: [user_id, hotel_id]
|
||||
properties:
|
||||
user_id:
|
||||
type: string
|
||||
description: Identifier of the user who should receive the offer.
|
||||
hotel_id:
|
||||
type: string
|
||||
description: Identifier of the hotel assigned to the user.
|
||||
RecentUsersResponse:
|
||||
description: Response containing the users array produced by /users/recent.
|
||||
type: object
|
||||
required: [users]
|
||||
properties:
|
||||
users:
|
||||
type: array
|
||||
description: |
|
||||
Recent users that should be copied into the users field of
|
||||
/segments/hotel. With the default limit this array usually contains
|
||||
up to 30 users.
|
||||
items:
|
||||
$ref: "#/components/schemas/User"
|
||||
TopHotelsResponse:
|
||||
description: Response containing the hotels array produced by /hotels/top.
|
||||
type: object
|
||||
required: [hotels]
|
||||
properties:
|
||||
hotels:
|
||||
type: array
|
||||
description: |
|
||||
Candidate hotels that should be copied into the hotels field of
|
||||
/segments/hotel. With the default limit this array usually contains
|
||||
up to 5 hotels.
|
||||
items:
|
||||
$ref: "#/components/schemas/Hotel"
|
||||
HotelSegmentsRequest:
|
||||
description: |
|
||||
Request body for building segments from users and hotels.
|
||||
This request combines the users array from /users/recent and the hotels array
|
||||
from /hotels/top.
|
||||
type: object
|
||||
required: [users, hotels]
|
||||
properties:
|
||||
users:
|
||||
type: array
|
||||
description: |
|
||||
Users from /users/recent. This is typically the same array of up to 30
|
||||
recent users returned by the first step. These users are being
|
||||
distributed across candidate hotels by preference.
|
||||
items:
|
||||
$ref: "#/components/schemas/User"
|
||||
hotels:
|
||||
type: array
|
||||
description: |
|
||||
Hotels from /hotels/top that should be used as candidate destinations
|
||||
for user distribution.
|
||||
items:
|
||||
$ref: "#/components/schemas/Hotel"
|
||||
HotelSegmentsResponse:
|
||||
description: Response containing the segments array produced by /segments/hotel.
|
||||
type: object
|
||||
required: [segments]
|
||||
properties:
|
||||
segments:
|
||||
type: array
|
||||
description: |
|
||||
Segments that should be copied into the segments field of
|
||||
/assignments/hotels.
|
||||
items:
|
||||
$ref: "#/components/schemas/Segment"
|
||||
AssignmentsRequest:
|
||||
description: |
|
||||
Request body for building assignments from the segments array returned by
|
||||
/segments/hotel.
|
||||
type: object
|
||||
required: [segments]
|
||||
properties:
|
||||
segments:
|
||||
type: array
|
||||
description: |
|
||||
Segments from /segments/hotel that should be converted into final
|
||||
user-to-hotel assignments.
|
||||
items:
|
||||
$ref: "#/components/schemas/Segment"
|
||||
AssignmentsResponse:
|
||||
description: Response containing the assignments array produced by /assignments/hotels.
|
||||
type: object
|
||||
required: [assignments]
|
||||
properties:
|
||||
assignments:
|
||||
type: array
|
||||
description: |
|
||||
Assignments that should be copied into the assignments field of
|
||||
/emails/send-offers.
|
||||
items:
|
||||
$ref: "#/components/schemas/Assignment"
|
||||
EmailOfferRequest:
|
||||
description: |
|
||||
Request body for sending offer emails from the assignments array returned
|
||||
by /assignments/hotels.
|
||||
type: object
|
||||
required: [template_id, assignments]
|
||||
properties:
|
||||
template_id:
|
||||
type: string
|
||||
default: offer_template_2026
|
||||
description: Identifier of the email template to use for every assignment in this request.
|
||||
assignments:
|
||||
type: array
|
||||
description: |
|
||||
Assignments from /assignments/hotels that should be emailed in the
|
||||
final step.
|
||||
items:
|
||||
$ref: "#/components/schemas/Assignment"
|
||||
EmailOfferResponse:
|
||||
description: |
|
||||
Result of the final email delivery step.
|
||||
This response does not contain new users, hotels, segments, or assignments.
|
||||
type: object
|
||||
required: [sent_count, failed_count, failed]
|
||||
properties:
|
||||
sent_count:
|
||||
type: integer
|
||||
description: Number of assignments for which an email was sent successfully.
|
||||
failed_count:
|
||||
type: integer
|
||||
description: Number of assignments for which email delivery failed.
|
||||
failed:
|
||||
type: array
|
||||
description: Failed deliveries with reasons for each affected user.
|
||||
items:
|
||||
type: object
|
||||
required: [user_id, reason]
|
||||
properties:
|
||||
user_id:
|
||||
type: string
|
||||
description: Identifier of the user whose email delivery failed.
|
||||
reason:
|
||||
type: string
|
||||
description: Human-readable explanation of why the email could not be sent.
|
||||
@@ -0,0 +1,2 @@
|
||||
fastapi>=0.115.0
|
||||
uvicorn[standard]>=0.30.0
|
||||
@@ -0,0 +1,52 @@
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app.main import app
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
def test_travel_linear_workflow() -> None:
|
||||
users = client.get("/users/recent", params={"limit": 4}).json()["users"]
|
||||
hotels = client.get("/hotels/top", params={"limit": 2}).json()["hotels"]
|
||||
|
||||
segments = client.post(
|
||||
"/segments/hotel",
|
||||
json={"users": users, "hotels": hotels},
|
||||
).json()["segments"]
|
||||
|
||||
assignments = client.post(
|
||||
"/assignments/hotels",
|
||||
json={"segments": segments},
|
||||
).json()["assignments"]
|
||||
|
||||
response = client.post(
|
||||
"/emails/send-offers",
|
||||
json={"template_id": "offer_template_2026", "assignments": assignments},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
body = response.json()
|
||||
assert body["failed_count"] == 0
|
||||
assert body["sent_count"] == len(assignments)
|
||||
|
||||
|
||||
def test_crm_linear_workflow() -> None:
|
||||
leads = client.get("/crm/leads/recent", params={"limit": 5}).json()["leads"]
|
||||
|
||||
qualified = client.post(
|
||||
"/crm/leads/qualify",
|
||||
json={"leads": leads},
|
||||
).json()["qualified_leads"]
|
||||
|
||||
offers = client.post(
|
||||
"/crm/offers/prepare",
|
||||
json={"qualified_leads": qualified},
|
||||
).json()["offers"]
|
||||
|
||||
response = client.post("/crm/offers/send", json={"offers": offers})
|
||||
|
||||
assert response.status_code == 200
|
||||
body = response.json()
|
||||
assert body["failed_count"] == 0
|
||||
assert body["sent_count"] == len(offers)
|
||||
Reference in New Issue
Block a user