first
This commit is contained in:
+53
-3
@@ -1,5 +1,18 @@
|
|||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
from fastapi import FastAPI
|
|
||||||
|
from fastapi import FastAPI, Header, HTTPException
|
||||||
|
|
||||||
|
from app.models import CreateCompanyRequest, CreateCompanyResponse
|
||||||
|
from app.raynet_client import (
|
||||||
|
RaynetAuthError,
|
||||||
|
RaynetClient,
|
||||||
|
RaynetError,
|
||||||
|
RaynetValidationError,
|
||||||
|
)
|
||||||
|
|
||||||
|
logging.basicConfig(level=os.getenv("LOG_LEVEL", "INFO"))
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
APP_NAME = os.getenv("APP_NAME", "raynet")
|
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")
|
||||||
@@ -8,18 +21,55 @@ ROOT_PATH = os.getenv("ROOT_PATH", "")
|
|||||||
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.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/health")
|
@app.get("/health")
|
||||||
def health():
|
def health():
|
||||||
return {"status": "ok"}
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/version")
|
@app.get("/version")
|
||||||
def version():
|
def version():
|
||||||
return {
|
return {
|
||||||
"app": APP_NAME,
|
"app": APP_NAME,
|
||||||
"version": APP_VERSION,
|
"version": APP_VERSION,
|
||||||
"language": "python",
|
"language": "python",
|
||||||
"root_path": ROOT_PATH
|
"root_path": ROOT_PATH,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/company", response_model=CreateCompanyResponse, tags=["company"])
|
||||||
|
def create_company(
|
||||||
|
request: CreateCompanyRequest,
|
||||||
|
x_api_key: str = Header(
|
||||||
|
...,
|
||||||
|
alias="X-Api-Key",
|
||||||
|
description="RAYNET API klíč (secret). Předává se výhradně hlavičkou.",
|
||||||
|
),
|
||||||
|
):
|
||||||
|
"""Vytvoří firmu v RAYNET CRM.
|
||||||
|
|
||||||
|
- **X-Api-Key** (hlavička, secret): API klíč z RAYNET (Nastavení > API).
|
||||||
|
- **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)
|
||||||
|
|||||||
+117
@@ -0,0 +1,117 @@
|
|||||||
|
"""Pydantic modely pro RAYNET endpointy.
|
||||||
|
|
||||||
|
Modely odpovídají RAYNET CRM API v2 (CompanyInsertDto). Slouží zároveň jako
|
||||||
|
zdroj pro Swagger/OpenAPI dokumentaci.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
|
class Rating(str, Enum):
|
||||||
|
A = "A"
|
||||||
|
B = "B"
|
||||||
|
C = "C"
|
||||||
|
|
||||||
|
|
||||||
|
class CompanyState(str, Enum):
|
||||||
|
A_POTENTIAL = "A_POTENTIAL"
|
||||||
|
B_ACTUAL = "B_ACTUAL"
|
||||||
|
C_DEFERRED = "C_DEFERRED"
|
||||||
|
D_UNATTRACTIVE = "D_UNATTRACTIVE"
|
||||||
|
|
||||||
|
|
||||||
|
class CompanyRole(str, Enum):
|
||||||
|
A_SUBSCRIBER = "A_SUBSCRIBER"
|
||||||
|
B_PARTNER = "B_PARTNER"
|
||||||
|
C_SUPPLIER = "C_SUPPLIER"
|
||||||
|
D_RIVAL = "D_RIVAL"
|
||||||
|
|
||||||
|
|
||||||
|
class TaxPayer(str, Enum):
|
||||||
|
YES = "YES"
|
||||||
|
NO = "NO"
|
||||||
|
|
||||||
|
|
||||||
|
class Address(BaseModel):
|
||||||
|
name: Optional[str] = Field(default=None, examples=["Sídlo klienta"])
|
||||||
|
street: Optional[str] = Field(default=None, examples=["Francouzská 6167/5"])
|
||||||
|
city: Optional[str] = Field(default=None, examples=["Ostrava"])
|
||||||
|
province: Optional[str] = Field(default=None, examples=["Morava"])
|
||||||
|
zipCode: Optional[str] = Field(default=None, examples=["708 00"])
|
||||||
|
country: Optional[str] = Field(default=None, examples=["CZ"])
|
||||||
|
lat: Optional[float] = None
|
||||||
|
lng: Optional[float] = None
|
||||||
|
|
||||||
|
|
||||||
|
class ContactInfo(BaseModel):
|
||||||
|
email: Optional[str] = Field(default=None, examples=["info@raynet.cz"])
|
||||||
|
email2: Optional[str] = None
|
||||||
|
fax: Optional[str] = None
|
||||||
|
tel1: Optional[str] = Field(default=None, examples=["+420 553 401 520"])
|
||||||
|
tel1Type: Optional[str] = Field(default=None, examples=["recepce"])
|
||||||
|
tel2: Optional[str] = None
|
||||||
|
tel2Type: Optional[str] = None
|
||||||
|
www: Optional[str] = Field(default=None, examples=["www.raynet.cz"])
|
||||||
|
doNotSendMM: Optional[bool] = None
|
||||||
|
otherContact: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class CompanyAddress(BaseModel):
|
||||||
|
address: Optional[Address] = None
|
||||||
|
contactInfo: Optional[ContactInfo] = None
|
||||||
|
|
||||||
|
|
||||||
|
class CompanyData(BaseModel):
|
||||||
|
"""Tělo požadavku pro vytvoření firmy (CompanyInsertDto).
|
||||||
|
|
||||||
|
Povinná pole: ``name``, ``rating``, ``state``, ``role``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Povinná pole
|
||||||
|
name: str = Field(..., examples=["ACME s.r.o."])
|
||||||
|
rating: Rating = Field(..., examples=[Rating.A])
|
||||||
|
state: CompanyState = Field(..., examples=[CompanyState.A_POTENTIAL])
|
||||||
|
role: CompanyRole = Field(..., examples=[CompanyRole.A_SUBSCRIBER])
|
||||||
|
|
||||||
|
# Volitelná pole
|
||||||
|
regNumber: Optional[str] = Field(default=None, examples=["12345678"])
|
||||||
|
taxNumber: Optional[str] = Field(default=None, examples=["CZ12345678"])
|
||||||
|
taxNumber2: Optional[str] = None
|
||||||
|
taxPayer: Optional[TaxPayer] = None
|
||||||
|
bankAccount: Optional[str] = None
|
||||||
|
notice: Optional[str] = None
|
||||||
|
owner: Optional[int] = Field(default=None, description="ID vlastníka (uživatele)")
|
||||||
|
category: Optional[int] = None
|
||||||
|
contactSource: Optional[int] = None
|
||||||
|
employeesNumber: Optional[int] = None
|
||||||
|
legalForm: Optional[int] = None
|
||||||
|
paymentTerm: Optional[int] = None
|
||||||
|
turnover: Optional[int] = None
|
||||||
|
territory: Optional[int] = None
|
||||||
|
addresses: Optional[list[CompanyAddress]] = None
|
||||||
|
tags: Optional[list[str]] = None
|
||||||
|
customFields: Optional[dict] = None
|
||||||
|
|
||||||
|
# Umožní i pole, která zde nejsou explicitně vyjmenovaná (RAYNET jich má víc).
|
||||||
|
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):
|
||||||
|
id: Optional[int] = Field(default=None, description="ID nově vytvořené firmy")
|
||||||
|
success: bool = True
|
||||||
|
raw: Optional[dict] = Field(default=None, description="Surová odpověď RAYNET API")
|
||||||
@@ -0,0 +1,262 @@
|
|||||||
|
"""Robustní konektor pro RAYNET CRM API (v2).
|
||||||
|
|
||||||
|
RAYNET API používá HTTP Basic Auth (username = email uživatele, password = API
|
||||||
|
klíč) a navíc povinnou hlavičku ``X-Instance-Name`` určující instanci CRM.
|
||||||
|
|
||||||
|
Tato třída je stateless wrapper nad ``requests.Session`` – jednu instanci
|
||||||
|
``RaynetClient`` je možné držet po dobu života aplikace a sdílet pro více volání.
|
||||||
|
|
||||||
|
POZOR (viz AGENTS.md): API klíč ani email se nikdy nesmí logovat. Logujeme pouze
|
||||||
|
URL, HTTP status a (zkrácené) tělo odpovědi při chybě.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from requests.adapters import HTTPAdapter
|
||||||
|
from requests.auth import HTTPBasicAuth
|
||||||
|
from urllib3.util.retry import Retry
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Výchozí produkční base URL RAYNET CRM API v2.
|
||||||
|
DEFAULT_BASE_URL = "https://app.raynet.cz/api/v2"
|
||||||
|
DEFAULT_TIMEOUT = 30 # sekundy
|
||||||
|
|
||||||
|
|
||||||
|
class RaynetError(Exception):
|
||||||
|
"""Základní výjimka konektoru.
|
||||||
|
|
||||||
|
Atributy:
|
||||||
|
message: lidsky čitelný popis chyby.
|
||||||
|
status_code: HTTP status (pokud byla obdržena odpověď serveru).
|
||||||
|
payload: parsované tělo odpovědi (pokud je k dispozici).
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
message: str,
|
||||||
|
status_code: Optional[int] = None,
|
||||||
|
payload: Any = None,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(message)
|
||||||
|
self.message = message
|
||||||
|
self.status_code = status_code
|
||||||
|
self.payload = payload
|
||||||
|
|
||||||
|
|
||||||
|
class RaynetAuthError(RaynetError):
|
||||||
|
"""Chyba autentizace / autorizace (HTTP 401 nebo 403)."""
|
||||||
|
|
||||||
|
|
||||||
|
class RaynetValidationError(RaynetError):
|
||||||
|
"""Server odmítl data (HTTP 4xx, typicky 400 – nevalidní vstup)."""
|
||||||
|
|
||||||
|
|
||||||
|
class RaynetClient:
|
||||||
|
"""Klient pro komunikaci s RAYNET CRM API.
|
||||||
|
|
||||||
|
Příklad použití::
|
||||||
|
|
||||||
|
client = RaynetClient(
|
||||||
|
api_key="...", # API klíč z RAYNET (Nastavení > API)
|
||||||
|
email="user@firma.cz", # email uživatele
|
||||||
|
instance_name="moje-instance",
|
||||||
|
)
|
||||||
|
company = client.create_company({"name": "ACME s.r.o."})
|
||||||
|
print(company["id"])
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
api_key: str,
|
||||||
|
email: str,
|
||||||
|
instance_name: str,
|
||||||
|
base_url: str = DEFAULT_BASE_URL,
|
||||||
|
timeout: int = DEFAULT_TIMEOUT,
|
||||||
|
max_retries: int = 3,
|
||||||
|
) -> None:
|
||||||
|
if not api_key or not email or not instance_name:
|
||||||
|
raise ValueError(
|
||||||
|
"RaynetClient vyžaduje neprázdné api_key, email a instance_name."
|
||||||
|
)
|
||||||
|
|
||||||
|
self.base_url = base_url.rstrip("/")
|
||||||
|
self.timeout = timeout
|
||||||
|
|
||||||
|
self._session = requests.Session()
|
||||||
|
self._session.auth = HTTPBasicAuth(email, api_key)
|
||||||
|
self._session.headers.update(
|
||||||
|
{
|
||||||
|
"X-Instance-Name": instance_name,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Automatické opakování pro přechodné chyby (síť, 429, 5xx).
|
||||||
|
retry = Retry(
|
||||||
|
total=max_retries,
|
||||||
|
backoff_factor=0.5,
|
||||||
|
status_forcelist=(429, 500, 502, 503, 504),
|
||||||
|
allowed_methods=frozenset({"GET", "POST", "PUT", "DELETE"}),
|
||||||
|
raise_on_status=False,
|
||||||
|
)
|
||||||
|
adapter = HTTPAdapter(max_retries=retry)
|
||||||
|
self._session.mount("https://", adapter)
|
||||||
|
self._session.mount("http://", adapter)
|
||||||
|
|
||||||
|
# Bez tajemství – logujeme jen instanci a base URL.
|
||||||
|
logger.info(
|
||||||
|
"RaynetClient inicializován (instance=%s, base_url=%s)",
|
||||||
|
instance_name,
|
||||||
|
self.base_url,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# Veřejné metody
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
def create_company(self, data: dict) -> dict:
|
||||||
|
"""Vytvoří firmu (company) v RAYNET CRM.
|
||||||
|
|
||||||
|
RAYNET používá pro vytvoření záznamu HTTP metodu ``PUT`` na kolekci
|
||||||
|
``/company/`` (nikoli POST).
|
||||||
|
|
||||||
|
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):
|
||||||
|
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:
|
||||||
|
raise ValueError(
|
||||||
|
"Firma musí mít vyplněná povinná pole: " + ", ".join(missing) + "."
|
||||||
|
)
|
||||||
|
|
||||||
|
return self._request("PUT", "/company/", json=data)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# Interní HTTP vrstva
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
def _request(self, method: str, path: str, **kwargs: Any) -> dict:
|
||||||
|
url = f"{self.base_url}/{path.lstrip('/')}"
|
||||||
|
kwargs.setdefault("timeout", self.timeout)
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = self._session.request(method, url, **kwargs)
|
||||||
|
except requests.exceptions.Timeout as exc:
|
||||||
|
logger.error("RAYNET timeout: %s %s", method, url)
|
||||||
|
raise RaynetError(f"Vypršel časový limit požadavku na {url}.") from exc
|
||||||
|
except requests.exceptions.ConnectionError as exc:
|
||||||
|
logger.error("RAYNET connection error: %s %s", method, url)
|
||||||
|
raise RaynetError(f"Nepodařilo se připojit k {url}.") from exc
|
||||||
|
except requests.exceptions.RequestException as exc:
|
||||||
|
# Síťová / nízkoúrovňová chyba requests, kterou jinak neumíme zařadit.
|
||||||
|
logger.error("RAYNET request error: %s %s – %s", method, url, exc)
|
||||||
|
raise RaynetError(f"Chyba požadavku na {url}: {exc}") from exc
|
||||||
|
|
||||||
|
return self._handle_response(response, method, url)
|
||||||
|
|
||||||
|
def _handle_response(
|
||||||
|
self, response: requests.Response, method: str, url: str
|
||||||
|
) -> dict:
|
||||||
|
# Pokus o parsování těla (i chybové odpovědi bývají JSON).
|
||||||
|
try:
|
||||||
|
payload: Any = response.json() if response.content else {}
|
||||||
|
except ValueError:
|
||||||
|
payload = {"raw": response.text}
|
||||||
|
|
||||||
|
if response.ok:
|
||||||
|
logger.info("RAYNET %s %s -> %s", method, url, response.status_code)
|
||||||
|
return payload if isinstance(payload, dict) else {"data": payload}
|
||||||
|
|
||||||
|
# Chybové stavy – zalogujeme bez tajemství (auth je v session, ne v logu).
|
||||||
|
snippet = str(payload)[:500]
|
||||||
|
logger.error(
|
||||||
|
"RAYNET %s %s selhalo se statusem %s: %s",
|
||||||
|
method,
|
||||||
|
url,
|
||||||
|
response.status_code,
|
||||||
|
snippet,
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code in (401, 403):
|
||||||
|
raise RaynetAuthError(
|
||||||
|
"Autentizace k RAYNET selhala – zkontroluj email, API klíč "
|
||||||
|
"a název instance.",
|
||||||
|
status_code=response.status_code,
|
||||||
|
payload=payload,
|
||||||
|
)
|
||||||
|
if 400 <= response.status_code < 500:
|
||||||
|
raise RaynetValidationError(
|
||||||
|
f"RAYNET odmítl požadavek (HTTP {response.status_code}).",
|
||||||
|
status_code=response.status_code,
|
||||||
|
payload=payload,
|
||||||
|
)
|
||||||
|
raise RaynetError(
|
||||||
|
f"RAYNET vrátil chybu serveru (HTTP {response.status_code}).",
|
||||||
|
status_code=response.status_code,
|
||||||
|
payload=payload,
|
||||||
|
)
|
||||||
|
|
||||||
|
def close(self) -> None:
|
||||||
|
"""Uvolní HTTP session."""
|
||||||
|
self._session.close()
|
||||||
|
|
||||||
|
def __enter__(self) -> "RaynetClient":
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, *exc: Any) -> None:
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Ukázková struktura požadavku pro create_company (RAYNET CRM API v2,
|
||||||
|
# model CompanyInsertDto, endpoint PUT /company/).
|
||||||
|
#
|
||||||
|
# Povinná pole: name, rating, state, role. Ostatní jsou volitelná.
|
||||||
|
# Číselníkové hodnoty (rating/state/role/taxPayer) odpovídají enumům RAYNET.
|
||||||
|
# Pole typu integer (owner, category, legalForm, territory, ...) jsou ID
|
||||||
|
# odkazující na číselníky ve vaší instanci – uveď jen pokud je znáš.
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
EXAMPLE_COMPANY = {
|
||||||
|
"name": "ACME s.r.o.",
|
||||||
|
"rating": "A", # A | B | C
|
||||||
|
"state": "A_POTENTIAL", # A_POTENTIAL | B_ACTUAL | C_DEFERRED | D_UNATTRACTIVE
|
||||||
|
"role": "A_SUBSCRIBER", # A_SUBSCRIBER | B_PARTNER | C_SUPPLIER | D_RIVAL
|
||||||
|
"regNumber": "12345678", # IČO
|
||||||
|
"taxNumber": "CZ12345678", # DIČ
|
||||||
|
"taxPayer": "YES", # YES | NO
|
||||||
|
"notice": "Vytvořeno přes konektor.",
|
||||||
|
"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"],
|
||||||
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
# 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")
|
||||||
|
```
|
||||||
+7
-2
@@ -1,2 +1,7 @@
|
|||||||
fastapi
|
# Připnuté verze = deterministický Docker build (viz AGENTS.md).
|
||||||
uvicorn[standard]
|
# Ověřeno: app se importuje a OpenAPI generuje s těmito verzemi.
|
||||||
|
fastapi==0.136.3
|
||||||
|
uvicorn[standard]==0.48.0
|
||||||
|
pydantic==2.12.2
|
||||||
|
requests==2.32.5
|
||||||
|
urllib3==2.5.0
|
||||||
|
|||||||
Reference in New Issue
Block a user