2
This commit is contained in:
+174
-33
@@ -1,9 +1,10 @@
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from fastapi import FastAPI, Header, HTTPException
|
from fastapi import Body, Depends, FastAPI, Header, HTTPException, Query, Request
|
||||||
|
|
||||||
from app.models import CreateCompanyRequest, CreateCompanyResponse
|
from app.models import CompanyData, CreateCompanyResponse
|
||||||
from app.raynet_client import (
|
from app.raynet_client import (
|
||||||
RaynetAuthError,
|
RaynetAuthError,
|
||||||
RaynetClient,
|
RaynetClient,
|
||||||
@@ -18,20 +19,90 @@ APP_NAME = os.getenv("APP_NAME", "raynet")
|
|||||||
APP_VERSION = os.getenv("APP_VERSION", "1.0.0")
|
APP_VERSION = os.getenv("APP_VERSION", "1.0.0")
|
||||||
ROOT_PATH = os.getenv("ROOT_PATH", "")
|
ROOT_PATH = os.getenv("ROOT_PATH", "")
|
||||||
|
|
||||||
|
# Popis se zobrazí nahoře ve Swaggeru (/docs) – přihlašovací údaje jsou
|
||||||
|
# společné pro celé připojení a posílají se v hlavičkách u KAŽDÉHO requestu.
|
||||||
|
API_DESCRIPTION = """
|
||||||
|
Stateless proxy nad **RAYNET CRM API v2**.
|
||||||
|
|
||||||
|
## Přihlášení (společné pro celé připojení)
|
||||||
|
|
||||||
|
Každý endpoint vyžaduje tři hlavičky. Jsou stejné pro všechna volání:
|
||||||
|
|
||||||
|
| Hlavička | Co to je | Kde to vzít |
|
||||||
|
|-------------------|--------------------|-------------|
|
||||||
|
| **`X-Api-Key`** | API klíč (secret) | V RAYNET CRM: **Nastavení → Klíč k API** (vygeneruj / zkopíruj). |
|
||||||
|
| **`X-Raynet-Email`** | Email uživatele | Přihlašovací email do RAYNET (tvoří dvojici s API klíčem pro Basic Auth). |
|
||||||
|
| **`X-Instance-Name`** | Název instance | Identifikátor tvé RAYNET instance (subdoména účtu). |
|
||||||
|
|
||||||
|
`X-Api-Key` je **secret** – nikdy se neloguje ani nevrací z endpointů.
|
||||||
|
|
||||||
|
## Endpointy
|
||||||
|
|
||||||
|
- Typované zkratky pro firmy: `POST/GET/PUT/DELETE /company...`
|
||||||
|
- Generický průchod na celé API: `/api/{resource}` – podporuje libovolnou
|
||||||
|
RAYNET entitu (`company`, `person`, `lead`, `businessCase`, `activity`,
|
||||||
|
`product`, `offer`, `order`, `project`, ...).
|
||||||
|
|
||||||
|
Plná dokumentace RAYNET: <https://app.raynetcrm.com/api/doc/index-en.html>
|
||||||
|
"""
|
||||||
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title=APP_NAME,
|
title=APP_NAME,
|
||||||
version=APP_VERSION,
|
version=APP_VERSION,
|
||||||
root_path=ROOT_PATH,
|
root_path=ROOT_PATH,
|
||||||
description="Stateless proxy nad RAYNET CRM API v2.",
|
description=API_DESCRIPTION,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/health")
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Sdílená credentials dependency – 3 hlavičky pro celé připojení.
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
def get_client(
|
||||||
|
x_api_key: str = Header(
|
||||||
|
..., alias="X-Api-Key", description="RAYNET API klíč (secret)."
|
||||||
|
),
|
||||||
|
x_raynet_email: str = Header(
|
||||||
|
..., alias="X-Raynet-Email", description="Email uživatele RAYNET."
|
||||||
|
),
|
||||||
|
x_instance_name: str = Header(
|
||||||
|
..., alias="X-Instance-Name", description="Název RAYNET instance."
|
||||||
|
),
|
||||||
|
):
|
||||||
|
"""Vytvoří RaynetClient z hlaviček a po dokončení requestu uvolní session."""
|
||||||
|
client = RaynetClient(
|
||||||
|
api_key=x_api_key,
|
||||||
|
email=x_raynet_email,
|
||||||
|
instance_name=x_instance_name,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
yield client
|
||||||
|
finally:
|
||||||
|
client.close()
|
||||||
|
|
||||||
|
|
||||||
|
def _run(fn, *args, **kwargs) -> Any:
|
||||||
|
"""Spustí volání klienta a převede výjimky konektoru na HTTP odpovědi."""
|
||||||
|
try:
|
||||||
|
return fn(*args, **kwargs)
|
||||||
|
except ValueError as exc:
|
||||||
|
raise HTTPException(status_code=422, detail=str(exc)) from exc
|
||||||
|
except RaynetAuthError as exc:
|
||||||
|
raise HTTPException(status_code=401, detail=exc.message) from exc
|
||||||
|
except RaynetValidationError as exc:
|
||||||
|
raise HTTPException(status_code=400, detail=exc.message) from exc
|
||||||
|
except RaynetError as exc:
|
||||||
|
raise HTTPException(status_code=502, detail=exc.message) from exc
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Servisní endpointy
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
@app.get("/health", tags=["service"])
|
||||||
def health():
|
def health():
|
||||||
return {"status": "ok"}
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/version")
|
@app.get("/version", tags=["service"])
|
||||||
def version():
|
def version():
|
||||||
return {
|
return {
|
||||||
"app": APP_NAME,
|
"app": APP_NAME,
|
||||||
@@ -41,35 +112,105 @@ def version():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Typované zkratky pro firmy (company)
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
@app.post("/company", response_model=CreateCompanyResponse, tags=["company"])
|
@app.post("/company", response_model=CreateCompanyResponse, tags=["company"])
|
||||||
def create_company(
|
def create_company(
|
||||||
request: CreateCompanyRequest,
|
company: CompanyData,
|
||||||
x_api_key: str = Header(
|
client: RaynetClient = Depends(get_client),
|
||||||
...,
|
|
||||||
alias="X-Api-Key",
|
|
||||||
description="RAYNET API klíč (secret). Předává se výhradně hlavičkou.",
|
|
||||||
),
|
|
||||||
):
|
):
|
||||||
"""Vytvoří firmu v RAYNET CRM.
|
"""Vytvoří firmu v RAYNET CRM (validovaná data dle CompanyInsertDto)."""
|
||||||
|
data = company.model_dump(exclude_none=True)
|
||||||
- **X-Api-Key** (hlavička, secret): API klíč z RAYNET (Nastavení > API).
|
result = _run(client.create_company, data)
|
||||||
- **email** / **instance_name** (tělo): identifikátory účtu a instance.
|
|
||||||
- **company** (tělo): data firmy dle RAYNET CompanyInsertDto.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
with RaynetClient(
|
|
||||||
api_key=x_api_key,
|
|
||||||
email=request.email,
|
|
||||||
instance_name=request.instance_name,
|
|
||||||
) as client:
|
|
||||||
# Posíláme jen vyplněná pole (žádné None), aby RAYNET nedostal prázdné hodnoty.
|
|
||||||
data = request.company.model_dump(exclude_none=True)
|
|
||||||
result = client.create_company(data)
|
|
||||||
except RaynetAuthError as exc:
|
|
||||||
raise HTTPException(status_code=401, detail=exc.message) from exc
|
|
||||||
except RaynetValidationError as exc:
|
|
||||||
raise HTTPException(status_code=400, detail=exc.message) from exc
|
|
||||||
except RaynetError as exc:
|
|
||||||
raise HTTPException(status_code=502, detail=exc.message) from exc
|
|
||||||
|
|
||||||
return CreateCompanyResponse(id=result.get("id"), success=True, raw=result)
|
return CreateCompanyResponse(id=result.get("id"), success=True, raw=result)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/company", tags=["company"])
|
||||||
|
def list_companies(
|
||||||
|
client: RaynetClient = Depends(get_client),
|
||||||
|
offset: int = Query(0, ge=0),
|
||||||
|
limit: int = Query(20, ge=1, le=1000),
|
||||||
|
fulltext: str | None = Query(None, description="Fulltextové hledání"),
|
||||||
|
):
|
||||||
|
"""Vrátí seznam firem s podporou stránkování a fulltextu."""
|
||||||
|
params = {"offset": offset, "limit": limit}
|
||||||
|
if fulltext:
|
||||||
|
params["fulltext"] = fulltext
|
||||||
|
return _run(client.list_companies, **params)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/company/{company_id}", tags=["company"])
|
||||||
|
def get_company(company_id: int, client: RaynetClient = Depends(get_client)):
|
||||||
|
"""Vrátí detail firmy."""
|
||||||
|
return _run(client.get_company, company_id)
|
||||||
|
|
||||||
|
|
||||||
|
@app.put("/company/{company_id}", tags=["company"])
|
||||||
|
def update_company(
|
||||||
|
company_id: int,
|
||||||
|
data: dict = Body(..., description="Pole firmy ke změně"),
|
||||||
|
client: RaynetClient = Depends(get_client),
|
||||||
|
):
|
||||||
|
"""Upraví firmu."""
|
||||||
|
return _run(client.update_company, company_id, data)
|
||||||
|
|
||||||
|
|
||||||
|
@app.delete("/company/{company_id}", tags=["company"])
|
||||||
|
def delete_company(company_id: int, client: RaynetClient = Depends(get_client)):
|
||||||
|
"""Smaže firmu."""
|
||||||
|
return _run(client.delete_company, company_id)
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Generický průchod na CELÉ RAYNET API
|
||||||
|
# resource = libovolná entita: company, person, lead, businessCase, activity,
|
||||||
|
# product, offer, order, project, ...
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
@app.get("/api/{resource}", tags=["generic"])
|
||||||
|
def api_list(
|
||||||
|
resource: str,
|
||||||
|
request: Request,
|
||||||
|
client: RaynetClient = Depends(get_client),
|
||||||
|
):
|
||||||
|
"""Seznam záznamů entity. Všechny query parametry se předávají do RAYNET
|
||||||
|
(např. `offset`, `limit`, `fulltext`, `name`, ...)."""
|
||||||
|
params = dict(request.query_params)
|
||||||
|
return _run(client.list_records, resource, params or None)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/{resource}/{record_id}", tags=["generic"])
|
||||||
|
def api_detail(
|
||||||
|
resource: str, record_id: str, client: RaynetClient = Depends(get_client)
|
||||||
|
):
|
||||||
|
"""Detail jednoho záznamu."""
|
||||||
|
return _run(client.get_record, resource, record_id)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/{resource}", tags=["generic"])
|
||||||
|
def api_create(
|
||||||
|
resource: str,
|
||||||
|
data: dict = Body(..., description="Data nového záznamu"),
|
||||||
|
client: RaynetClient = Depends(get_client),
|
||||||
|
):
|
||||||
|
"""Vytvoří záznam (mapuje se na RAYNET `PUT /{resource}/`)."""
|
||||||
|
return _run(client.create_record, resource, data)
|
||||||
|
|
||||||
|
|
||||||
|
@app.put("/api/{resource}/{record_id}", tags=["generic"])
|
||||||
|
def api_update(
|
||||||
|
resource: str,
|
||||||
|
record_id: str,
|
||||||
|
data: dict = Body(..., description="Pole ke změně"),
|
||||||
|
client: RaynetClient = Depends(get_client),
|
||||||
|
):
|
||||||
|
"""Upraví záznam (mapuje se na RAYNET `POST /{resource}/{id}/`)."""
|
||||||
|
return _run(client.update_record, resource, record_id, data)
|
||||||
|
|
||||||
|
|
||||||
|
@app.delete("/api/{resource}/{record_id}", tags=["generic"])
|
||||||
|
def api_delete(
|
||||||
|
resource: str, record_id: str, client: RaynetClient = Depends(get_client)
|
||||||
|
):
|
||||||
|
"""Smaže záznam."""
|
||||||
|
return _run(client.delete_record, resource, record_id)
|
||||||
|
|||||||
@@ -99,18 +99,6 @@ class CompanyData(BaseModel):
|
|||||||
model_config = {"extra": "allow"}
|
model_config = {"extra": "allow"}
|
||||||
|
|
||||||
|
|
||||||
class CreateCompanyRequest(BaseModel):
|
|
||||||
"""Vstup endpointu /company.
|
|
||||||
|
|
||||||
API klíč (secret) se předává hlavičkou ``X-Api-Key`` – NENÍ součástí tohoto
|
|
||||||
těla. Email a název instance jsou běžné identifikátory, proto jdou v těle.
|
|
||||||
"""
|
|
||||||
|
|
||||||
email: str = Field(..., examples=["user@firma.cz"], description="Email uživatele RAYNET")
|
|
||||||
instance_name: str = Field(..., examples=["moje-instance"], description="Název RAYNET instance")
|
|
||||||
company: CompanyData
|
|
||||||
|
|
||||||
|
|
||||||
class CreateCompanyResponse(BaseModel):
|
class CreateCompanyResponse(BaseModel):
|
||||||
id: Optional[int] = Field(default=None, description="ID nově vytvořené firmy")
|
id: Optional[int] = Field(default=None, description="ID nově vytvořené firmy")
|
||||||
success: bool = True
|
success: bool = True
|
||||||
|
|||||||
+79
-21
@@ -115,41 +115,99 @@ class RaynetClient:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
# Veřejné metody
|
# Generické CRUD nad libovolnou RAYNET entitou (resource)
|
||||||
|
#
|
||||||
|
# RAYNET má napříč celým API jednotné konvence:
|
||||||
|
# list GET /{resource}/
|
||||||
|
# detail GET /{resource}/{id}/
|
||||||
|
# create PUT /{resource}/
|
||||||
|
# update POST /{resource}/{id}/
|
||||||
|
# delete DELETE /{resource}/{id}/
|
||||||
|
# Tyto metody proto pokrývají celé RAYNET API (company, person, lead,
|
||||||
|
# businessCase, activity, product, offer, order, project, ...).
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
def list_records(self, resource: str, params: Optional[dict] = None) -> dict:
|
||||||
|
"""Vrátí seznam záznamů dané entity.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
resource: název entity, např. ``"company"``, ``"person"``, ``"lead"``.
|
||||||
|
params: filtry / stránkování (``offset``, ``limit``, ``fulltext``, ...).
|
||||||
|
"""
|
||||||
|
return self._request("GET", f"/{self._res(resource)}/", params=params)
|
||||||
|
|
||||||
|
def get_record(self, resource: str, record_id: int | str) -> dict:
|
||||||
|
"""Vrátí detail jednoho záznamu."""
|
||||||
|
return self._request("GET", f"/{self._res(resource)}/{record_id}/")
|
||||||
|
|
||||||
|
def create_record(self, resource: str, data: dict) -> dict:
|
||||||
|
"""Vytvoří záznam (RAYNET používá pro create metodu PUT)."""
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
raise ValueError("Parametr 'data' musí být slovník (dict).")
|
||||||
|
return self._request("PUT", f"/{self._res(resource)}/", json=data)
|
||||||
|
|
||||||
|
def update_record(self, resource: str, record_id: int | str, data: dict) -> dict:
|
||||||
|
"""Upraví záznam (RAYNET používá pro update metodu POST)."""
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
raise ValueError("Parametr 'data' musí být slovník (dict).")
|
||||||
|
return self._request("POST", f"/{self._res(resource)}/{record_id}/", json=data)
|
||||||
|
|
||||||
|
def delete_record(self, resource: str, record_id: int | str) -> dict:
|
||||||
|
"""Smaže záznam."""
|
||||||
|
return self._request("DELETE", f"/{self._res(resource)}/{record_id}/")
|
||||||
|
|
||||||
|
def call(
|
||||||
|
self,
|
||||||
|
method: str,
|
||||||
|
path: str,
|
||||||
|
params: Optional[dict] = None,
|
||||||
|
json: Optional[Any] = None,
|
||||||
|
) -> dict:
|
||||||
|
"""Univerzální volání pro vnořené zdroje a speciální akce.
|
||||||
|
|
||||||
|
Příklady cest: ``/company/{id}/address/``,
|
||||||
|
``/company/{id}/lock``, ``/company/{id}/merge/{sourceId}/``.
|
||||||
|
"""
|
||||||
|
return self._request(method.upper(), path, params=params, json=json)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# Pojmenované zkratky pro nejčastější entity (tenké wrappery)
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
def create_company(self, data: dict) -> dict:
|
def create_company(self, data: dict) -> dict:
|
||||||
"""Vytvoří firmu (company) v RAYNET CRM.
|
"""Vytvoří firmu (company) v RAYNET CRM.
|
||||||
|
|
||||||
RAYNET používá pro vytvoření záznamu HTTP metodu ``PUT`` na kolekci
|
Povinná pole: ``name``, ``rating``, ``state``, ``role``.
|
||||||
``/company/`` (nikoli POST).
|
Příklad struktury viz :data:`EXAMPLE_COMPANY` níže.
|
||||||
|
|
||||||
Args:
|
|
||||||
data: tělo požadavku dle RAYNET dokumentace. Povinná pole jsou
|
|
||||||
``name``, ``rating``, ``state`` a ``role``. Příklad kompletní
|
|
||||||
struktury viz :data:`EXAMPLE_COMPANY` níže.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Parsovaná JSON odpověď serveru (obsahuje mj. ``id`` nové firmy).
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: pokud ``data`` nejsou slovník nebo chybí povinná pole.
|
|
||||||
RaynetAuthError: při chybě autentizace (401/403).
|
|
||||||
RaynetValidationError: při zamítnutí dat serverem (4xx).
|
|
||||||
RaynetError: při ostatních chybách (síť, 5xx, nevalidní JSON).
|
|
||||||
"""
|
"""
|
||||||
if not isinstance(data, dict):
|
missing = [f for f in ("name", "rating", "state", "role") if not (data or {}).get(f)]
|
||||||
raise ValueError("Parametr 'data' musí být slovník (dict).")
|
|
||||||
missing = [f for f in ("name", "rating", "state", "role") if not data.get(f)]
|
|
||||||
if missing:
|
if missing:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"Firma musí mít vyplněná povinná pole: " + ", ".join(missing) + "."
|
"Firma musí mít vyplněná povinná pole: " + ", ".join(missing) + "."
|
||||||
)
|
)
|
||||||
|
return self.create_record("company", data)
|
||||||
|
|
||||||
return self._request("PUT", "/company/", json=data)
|
def list_companies(self, **params: Any) -> dict:
|
||||||
|
return self.list_records("company", params or None)
|
||||||
|
|
||||||
|
def get_company(self, company_id: int | str) -> dict:
|
||||||
|
return self.get_record("company", company_id)
|
||||||
|
|
||||||
|
def update_company(self, company_id: int | str, data: dict) -> dict:
|
||||||
|
return self.update_record("company", company_id, data)
|
||||||
|
|
||||||
|
def delete_company(self, company_id: int | str) -> dict:
|
||||||
|
return self.delete_record("company", company_id)
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
# Interní HTTP vrstva
|
# Interní HTTP vrstva
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
|
@staticmethod
|
||||||
|
def _res(resource: str) -> str:
|
||||||
|
"""Očistí název resource (bez lomítek), ať nejde sestavit divná URL."""
|
||||||
|
cleaned = (resource or "").strip().strip("/")
|
||||||
|
if not cleaned:
|
||||||
|
raise ValueError("Název resource nesmí být prázdný.")
|
||||||
|
return cleaned
|
||||||
|
|
||||||
def _request(self, method: str, path: str, **kwargs: Any) -> dict:
|
def _request(self, method: str, path: str, **kwargs: Any) -> dict:
|
||||||
url = f"{self.base_url}/{path.lstrip('/')}"
|
url = f"{self.base_url}/{path.lstrip('/')}"
|
||||||
kwargs.setdefault("timeout", self.timeout)
|
kwargs.setdefault("timeout", self.timeout)
|
||||||
|
|||||||
@@ -1,134 +0,0 @@
|
|||||||
# RAYNET konektor – vytvoření firmy
|
|
||||||
|
|
||||||
Stateless proxy nad [RAYNET CRM API v2](https://app.raynetcrm.com/api/doc/index-en.html).
|
|
||||||
|
|
||||||
## Autentizace RAYNET
|
|
||||||
|
|
||||||
RAYNET API používá:
|
|
||||||
|
|
||||||
- **HTTP Basic Auth**: username = email uživatele, password = **API klíč**
|
|
||||||
- hlavičku **`X-Instance-Name`** = název instance
|
|
||||||
- base URL: `https://app.raynet.cz/api/v2`
|
|
||||||
|
|
||||||
V této službě se **API klíč (secret) předává hlavičkou `X-Api-Key`**. Email a název
|
|
||||||
instance jsou běžné identifikátory a předávají se v těle požadavku.
|
|
||||||
|
|
||||||
> Secret se nikdy neloguje, necommituje ani nevrací z endpointů (viz AGENTS.md).
|
|
||||||
|
|
||||||
## Endpoint
|
|
||||||
|
|
||||||
```text
|
|
||||||
POST /company
|
|
||||||
```
|
|
||||||
|
|
||||||
Veřejně přes AppFactory reverse proxy:
|
|
||||||
|
|
||||||
```text
|
|
||||||
POST https://services.csbot.cz/apps/<app-id>/company
|
|
||||||
```
|
|
||||||
|
|
||||||
### Hlavičky
|
|
||||||
|
|
||||||
| Hlavička | Povinná | Popis |
|
|
||||||
|--------------|---------|-------------------------------|
|
|
||||||
| `X-Api-Key` | ano | RAYNET API klíč (secret) |
|
|
||||||
|
|
||||||
### Tělo požadavku
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"email": "user@firma.cz",
|
|
||||||
"instance_name": "moje-instance",
|
|
||||||
"company": {
|
|
||||||
"name": "ACME s.r.o.",
|
|
||||||
"rating": "A",
|
|
||||||
"state": "A_POTENTIAL",
|
|
||||||
"role": "A_SUBSCRIBER",
|
|
||||||
"regNumber": "12345678",
|
|
||||||
"taxNumber": "CZ12345678",
|
|
||||||
"taxPayer": "YES",
|
|
||||||
"addresses": [
|
|
||||||
{
|
|
||||||
"address": {
|
|
||||||
"name": "Sídlo klienta",
|
|
||||||
"street": "Francouzská 6167/5",
|
|
||||||
"city": "Ostrava",
|
|
||||||
"province": "Morava",
|
|
||||||
"zipCode": "708 00",
|
|
||||||
"country": "CZ"
|
|
||||||
},
|
|
||||||
"contactInfo": {
|
|
||||||
"email": "info@acme.cz",
|
|
||||||
"tel1": "+420 553 401 520",
|
|
||||||
"tel1Type": "recepce",
|
|
||||||
"www": "www.acme.cz"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"tags": ["import", "konektor"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Povinná pole firmy
|
|
||||||
|
|
||||||
| Pole | Typ | Hodnoty |
|
|
||||||
|----------|--------|---------------------------------------------------------------|
|
|
||||||
| `name` | string | název firmy |
|
|
||||||
| `rating` | enum | `A`, `B`, `C` |
|
|
||||||
| `state` | enum | `A_POTENTIAL`, `B_ACTUAL`, `C_DEFERRED`, `D_UNATTRACTIVE` |
|
|
||||||
| `role` | enum | `A_SUBSCRIBER`, `B_PARTNER`, `C_SUPPLIER`, `D_RIVAL` |
|
|
||||||
|
|
||||||
Volitelná pole: `regNumber` (IČO), `taxNumber` (DIČ), `taxPayer` (`YES`/`NO`),
|
|
||||||
`bankAccount`, `notice`, `owner`, `category`, `legalForm`, `paymentTerm`,
|
|
||||||
`turnover`, `territory`, `addresses`, `tags`, `customFields` a další dle
|
|
||||||
RAYNET `CompanyInsertDto`. Neuvedená/`null` pole se do RAYNET neposílají.
|
|
||||||
|
|
||||||
### Odpověď (200)
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": 123456,
|
|
||||||
"success": true,
|
|
||||||
"raw": { "...": "surová odpověď RAYNET" }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Chybové stavy
|
|
||||||
|
|
||||||
| HTTP | Kdy |
|
|
||||||
|------|------------------------------------------------------------|
|
|
||||||
| 400 | RAYNET odmítl data (nevalidní vstup) |
|
|
||||||
| 401 | chybný API klíč / email / instance (autentizace selhala) |
|
|
||||||
| 422 | nevalidní tělo požadavku (FastAPI validace) |
|
|
||||||
| 502 | RAYNET nedostupný / chyba serveru / timeout |
|
|
||||||
|
|
||||||
## Použití konektoru přímo v Pythonu
|
|
||||||
|
|
||||||
```python
|
|
||||||
from app.raynet_client import RaynetClient, RaynetError
|
|
||||||
|
|
||||||
client = RaynetClient(api_key="...", email="user@firma.cz", instance_name="moje-instance")
|
|
||||||
try:
|
|
||||||
company = client.create_company({
|
|
||||||
"name": "ACME s.r.o.",
|
|
||||||
"rating": "A",
|
|
||||||
"state": "A_POTENTIAL",
|
|
||||||
"role": "A_SUBSCRIBER",
|
|
||||||
})
|
|
||||||
print(company["id"])
|
|
||||||
except RaynetError as exc:
|
|
||||||
print("Chyba:", exc.message, exc.status_code)
|
|
||||||
```
|
|
||||||
|
|
||||||
`RaynetClient` drží `requests.Session` (Basic Auth + `X-Instance-Name`),
|
|
||||||
automaticky opakuje přechodné chyby (síť, 429, 5xx) a vyhazuje typované
|
|
||||||
výjimky `RaynetAuthError` / `RaynetValidationError` / `RaynetError`.
|
|
||||||
|
|
||||||
## Ověření po nasazení
|
|
||||||
|
|
||||||
```text
|
|
||||||
GET https://services.csbot.cz/apps/<app-id>/health
|
|
||||||
GET https://services.csbot.cz/apps/<app-id>/docs
|
|
||||||
POST https://services.csbot.cz/apps/<app-id>/company (Swagger „Try it out")
|
|
||||||
```
|
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
# RAYNET konektor
|
||||||
|
|
||||||
|
Stateless proxy nad [RAYNET CRM API v2](https://app.raynetcrm.com/api/doc/index-en.html).
|
||||||
|
Pokrývá celé RAYNET API díky jednotným REST konvencím.
|
||||||
|
|
||||||
|
## Přihlášení (společné pro celé připojení)
|
||||||
|
|
||||||
|
RAYNET API používá HTTP Basic Auth (email + API klíč) a hlavičku s názvem
|
||||||
|
instance. V této službě se předává **třemi hlavičkami u každého requestu**:
|
||||||
|
|
||||||
|
| Hlavička | Co to je | Kde to vzít |
|
||||||
|
|-----------------------|-------------------|-------------|
|
||||||
|
| `X-Api-Key` | API klíč (secret) | RAYNET CRM: **Nastavení → Klíč k API** |
|
||||||
|
| `X-Raynet-Email` | Email uživatele | Přihlašovací email do RAYNET |
|
||||||
|
| `X-Instance-Name` | Název instance | Identifikátor instance (subdoména účtu) |
|
||||||
|
|
||||||
|
> `X-Api-Key` je secret – nikdy se neloguje, necommituje ani nevrací z endpointů
|
||||||
|
> (viz AGENTS.md). Tyto údaje jsou zobrazené i nahoře ve Swaggeru (`/docs`).
|
||||||
|
|
||||||
|
Base URL RAYNET: `https://app.raynet.cz/api/v2`.
|
||||||
|
|
||||||
|
## REST konvence RAYNET (platí pro celé API)
|
||||||
|
|
||||||
|
| Operace | RAYNET metoda + cesta |
|
||||||
|
|---------|------------------------------|
|
||||||
|
| List | `GET /{resource}/` |
|
||||||
|
| Detail | `GET /{resource}/{id}/` |
|
||||||
|
| Create | `PUT /{resource}/` |
|
||||||
|
| Update | `POST /{resource}/{id}/` |
|
||||||
|
| Delete | `DELETE /{resource}/{id}/` |
|
||||||
|
|
||||||
|
Příklady entit: `company`, `person`, `lead`, `businessCase`, `activity`,
|
||||||
|
`product`, `offer`, `order`, `project`, …
|
||||||
|
|
||||||
|
## Endpointy služby
|
||||||
|
|
||||||
|
Veřejně přes AppFactory reverse proxy: `https://services.csbot.cz/apps/<app-id>/...`
|
||||||
|
|
||||||
|
### Typované zkratky pro firmy
|
||||||
|
|
||||||
|
| Metoda | Cesta | Popis |
|
||||||
|
|--------|-------------------------|-------------------------------|
|
||||||
|
| POST | `/company` | Vytvoří firmu (validace) |
|
||||||
|
| GET | `/company` | Seznam firem (offset/limit/fulltext) |
|
||||||
|
| GET | `/company/{id}` | Detail firmy |
|
||||||
|
| PUT | `/company/{id}` | Úprava firmy |
|
||||||
|
| DELETE | `/company/{id}` | Smazání firmy |
|
||||||
|
|
||||||
|
### Generický průchod na celé API
|
||||||
|
|
||||||
|
| Metoda | Cesta | Mapuje se na RAYNET |
|
||||||
|
|--------|-----------------------------|-------------------------------|
|
||||||
|
| GET | `/api/{resource}` | `GET /{resource}/` (query params se předávají) |
|
||||||
|
| GET | `/api/{resource}/{id}` | `GET /{resource}/{id}/` |
|
||||||
|
| POST | `/api/{resource}` | `PUT /{resource}/` (create) |
|
||||||
|
| PUT | `/api/{resource}/{id}` | `POST /{resource}/{id}/` (update) |
|
||||||
|
| DELETE | `/api/{resource}/{id}` | `DELETE /{resource}/{id}/` |
|
||||||
|
|
||||||
|
## Vytvoření firmy – povinná pole
|
||||||
|
|
||||||
|
| Pole | Typ | Hodnoty |
|
||||||
|
|----------|--------|------------------------------------------------------------|
|
||||||
|
| `name` | string | název firmy |
|
||||||
|
| `rating` | enum | `A`, `B`, `C` |
|
||||||
|
| `state` | enum | `A_POTENTIAL`, `B_ACTUAL`, `C_DEFERRED`, `D_UNATTRACTIVE` |
|
||||||
|
| `role` | enum | `A_SUBSCRIBER`, `B_PARTNER`, `C_SUPPLIER`, `D_RIVAL` |
|
||||||
|
|
||||||
|
Volitelná: `regNumber` (IČO), `taxNumber` (DIČ), `taxPayer` (`YES`/`NO`),
|
||||||
|
`bankAccount`, `notice`, `owner`, `category`, `legalForm`, `paymentTerm`,
|
||||||
|
`turnover`, `territory`, `addresses`, `tags`, `customFields` a další dle
|
||||||
|
RAYNET `CompanyInsertDto`.
|
||||||
|
|
||||||
|
### Příklad těla `POST /company`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "ACME s.r.o.",
|
||||||
|
"rating": "A",
|
||||||
|
"state": "A_POTENTIAL",
|
||||||
|
"role": "A_SUBSCRIBER",
|
||||||
|
"regNumber": "12345678",
|
||||||
|
"taxNumber": "CZ12345678",
|
||||||
|
"taxPayer": "YES",
|
||||||
|
"addresses": [
|
||||||
|
{
|
||||||
|
"address": {
|
||||||
|
"name": "Sídlo klienta",
|
||||||
|
"street": "Francouzská 6167/5",
|
||||||
|
"city": "Ostrava",
|
||||||
|
"province": "Morava",
|
||||||
|
"zipCode": "708 00",
|
||||||
|
"country": "CZ"
|
||||||
|
},
|
||||||
|
"contactInfo": {
|
||||||
|
"email": "info@acme.cz",
|
||||||
|
"tel1": "+420 553 401 520",
|
||||||
|
"tel1Type": "recepce",
|
||||||
|
"www": "www.acme.cz"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": ["import", "konektor"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Chybové stavy
|
||||||
|
|
||||||
|
| HTTP | Kdy |
|
||||||
|
|------|-----------------------------------------------------------|
|
||||||
|
| 400 | RAYNET odmítl data (nevalidní vstup) |
|
||||||
|
| 401 | chybný API klíč / email / instance (autentizace selhala) |
|
||||||
|
| 422 | nevalidní vstup (FastAPI validace nebo chybí povinné pole)|
|
||||||
|
| 502 | RAYNET nedostupný / chyba serveru / timeout |
|
||||||
|
|
||||||
|
## Použití konektoru přímo v Pythonu
|
||||||
|
|
||||||
|
```python
|
||||||
|
from app.raynet_client import RaynetClient, RaynetError
|
||||||
|
|
||||||
|
client = RaynetClient(api_key="...", email="user@firma.cz", instance_name="moje-instance")
|
||||||
|
try:
|
||||||
|
# pojmenovaná zkratka
|
||||||
|
company = client.create_company({
|
||||||
|
"name": "ACME s.r.o.", "rating": "A",
|
||||||
|
"state": "A_POTENTIAL", "role": "A_SUBSCRIBER",
|
||||||
|
})
|
||||||
|
|
||||||
|
# generické CRUD nad libovolnou entitou
|
||||||
|
leads = client.list_records("lead", {"limit": 50})
|
||||||
|
deal = client.get_record("businessCase", 123)
|
||||||
|
client.update_record("person", 42, {"lastName": "Novák"})
|
||||||
|
|
||||||
|
# speciální akce / vnořené zdroje
|
||||||
|
client.call("POST", f"/company/{company['id']}/lock")
|
||||||
|
except RaynetError as exc:
|
||||||
|
print("Chyba:", exc.message, exc.status_code)
|
||||||
|
```
|
||||||
|
|
||||||
|
`RaynetClient` drží `requests.Session` (Basic Auth + `X-Instance-Name`),
|
||||||
|
automaticky opakuje přechodné chyby (síť, 429, 5xx) a vyhazuje typované
|
||||||
|
výjimky `RaynetAuthError` / `RaynetValidationError` / `RaynetError`.
|
||||||
|
|
||||||
|
## Ověření po nasazení
|
||||||
|
|
||||||
|
```text
|
||||||
|
GET https://services.csbot.cz/apps/<app-id>/health
|
||||||
|
GET https://services.csbot.cz/apps/<app-id>/docs
|
||||||
|
```
|
||||||
|
|
||||||
|
Ve Swagger UI ověř, že `Try it out` volá endpointy přes `/apps/<app-id>` a že
|
||||||
|
jsou vyžadované hlavičky `X-Api-Key`, `X-Raynet-Email`, `X-Instance-Name`.
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user