Files
JiriUhlir 0e05fef335 first
2026-07-16 11:56:15 +02:00

225 lines
8.0 KiB
Python

"""Objednávky přepravy / svozu — tvorba (batch), stav, vyhledání, zrušení.
Stejně jako zásilky jsou objednávky asynchronní: POST order/batch vrátí batchId
(Location hlavička), stav zpracování se polluje přes GET order/batch/{batchId}.
Typy objednávek (pole orderType v těle):
- CollectionOrder — svoz z registrované adresy zákazníka (bez recipient)
- TransportOrder — přeprava z libovolné adresy (sender i recipient povinné)
"""
import asyncio
import time
from typing import Any
from fastapi import APIRouter, Body, Depends, Query
from ..config import (
BATCH_POLL_INTERVAL_SECONDS,
BATCH_WAIT_TIMEOUT_SECONDS,
TRANSLITERATE_DEFAULT,
)
from ..cpl_client import (
batch_id_from_location,
cpl_request,
ensure_success,
relay_json,
)
from ..credentials import Credentials, get_credentials
from ..logging_config import get_logger
from ..transliterate import transliterate_json
log = get_logger("pplcpl.orders")
router = APIRouter(prefix="/orders", tags=["orders"])
_EXAMPLE_ORDER_BODY = {
"orders": [
{
"orderType": "TransportOrder",
"referenceId": "ORD-0001",
"productType": "BUSS",
"shipmentCount": 1,
"sendDate": "2026-07-17",
"customerReference": "Zakazka 123",
"email": "odesilatel@example.com",
"sender": {
"name": "Firma s.r.o.",
"street": "Prazska 123/4",
"city": "Praha",
"zipCode": "10000",
"country": "CZ",
"phone": "+420601123456",
},
"recipient": {
"name": "Jan Novak",
"street": "Brnenska 10",
"city": "Brno",
"zipCode": "60200",
"country": "CZ",
"phone": "+420602123456",
},
}
]
}
_PENDING_STATES = ("Accepted", "InProcess")
def _maybe_transliterate(body: dict, transliterate: bool | None) -> dict:
apply = TRANSLITERATE_DEFAULT if transliterate is None else transliterate
return transliterate_json(body) if apply else body
@router.post(
"/batch",
status_code=201,
summary="Vytvoření objednávky přepravy / svozu (asynchronní)",
)
async def create_order_batch(
body: dict = Body(..., examples=[_EXAMPLE_ORDER_BODY]),
transliterate: bool | None = Query(default=None),
creds: Credentials = Depends(get_credentials),
):
"""`POST order/batch` — vrací batchId z Location hlavičky (max 100 objednávek)."""
resp = await cpl_request(
creds, "POST", "/order/batch", json_body=_maybe_transliterate(body, transliterate)
)
ensure_success(resp)
return {
"batchId": batch_id_from_location(resp),
"location": resp.headers.get("location"),
"correlationId": resp.headers.get("x-correlation-id"),
}
@router.get("/batch/{batch_id}", summary="Stav zpracování objednávky (batch)")
async def get_order_batch_status(
batch_id: str,
creds: Credentials = Depends(get_credentials),
):
"""`GET order/batch/{batchId}` — importState: Accepted/InProcess/Complete/Error."""
resp = await cpl_request(creds, "GET", f"/order/batch/{batch_id}")
return relay_json(resp)
@router.post(
"/create-and-wait",
summary="Vytvoření objednávky a počkání na zpracování (synchronní obálka)",
)
async def create_order_and_wait(
body: dict = Body(..., examples=[_EXAMPLE_ORDER_BODY]),
timeout_seconds: float = Query(default=BATCH_WAIT_TIMEOUT_SECONDS, ge=1, le=120),
transliterate: bool | None = Query(default=None),
creds: Credentials = Depends(get_credentials),
):
"""Convenience: POST order/batch + polling stavu v jednom requestu.
`completed=false` znamená timeout — zpracování v PPL běží dál."""
create_resp = await cpl_request(
creds, "POST", "/order/batch", json_body=_maybe_transliterate(body, transliterate)
)
ensure_success(create_resp)
batch_id = batch_id_from_location(create_resp)
deadline = time.monotonic() + timeout_seconds
status_data: dict = {}
completed = False
while True:
status_resp = await cpl_request(creds, "GET", f"/order/batch/{batch_id}")
ensure_success(status_resp)
status_data = status_resp.json() or {}
items = status_data.get("items") or []
pending = [i for i in items if i.get("importState") in _PENDING_STATES]
if items and not pending:
completed = True
break
if time.monotonic() >= deadline:
log.warning(
"Order batch %s nebyl zpracován do %ss, vracím completed=false.",
batch_id,
timeout_seconds,
)
break
await asyncio.sleep(BATCH_POLL_INTERVAL_SECONDS)
return {"batchId": batch_id, "completed": completed, **status_data}
@router.get("", summary="Vyhledání objednávek přepravy")
async def find_orders(
shipment_numbers: list[str] | None = Query(default=None, alias="shipmentNumbers"),
customer_references: list[str] | None = Query(
default=None, alias="customerReferences"
),
order_references: list[str] | None = Query(
default=None,
alias="orderReferences",
description="referenceId hodnoty z POST order/batch.",
),
order_numbers: list[str] | None = Query(default=None, alias="orderNumbers"),
order_ids: list[int] | None = Query(default=None, alias="orderIds"),
date_from: str | None = Query(default=None, alias="dateFrom"),
date_to: str | None = Query(default=None, alias="dateTo"),
send_date: str | None = Query(default=None, alias="sendDate"),
product_type: str | None = Query(default=None, alias="productType"),
order_states: str | None = Query(
default=None,
alias="orderStates",
description="None, Created, PickedUp, NotPickedUp, Canceled.",
),
order_type: str | None = Query(
default=None, alias="orderType", description="CollectionOrder / TransportOrder."
),
limit: int = Query(default=100, ge=1, le=1000),
offset: int = Query(default=0, ge=0),
creds: Credentials = Depends(get_credentials),
):
"""`GET order` — informace o objednávkách vč. stavu a přidělených čísel zásilek."""
params: dict[str, Any] = {"Limit": limit, "Offset": offset}
if shipment_numbers:
params["ShipmentNumbers"] = shipment_numbers
if customer_references:
params["CustomerReferences"] = customer_references
if order_references:
params["OrderReferences"] = order_references
if order_numbers:
params["OrderNumbers"] = order_numbers
if order_ids:
params["OrderIds"] = order_ids
if date_from:
params["DateFrom"] = date_from
if date_to:
params["DateTo"] = date_to
if send_date:
params["SendDate"] = send_date
if product_type:
params["ProductType"] = product_type
if order_states:
params["OrderStates"] = order_states
if order_type:
params["OrderType"] = order_type
resp = await cpl_request(creds, "GET", "/order", params=params)
return relay_json(resp)
@router.post("/cancel", summary="Zrušení objednávky svozu / přepravy")
async def cancel_order(
customer_reference: str | None = Query(
default=None, alias="customerReference", description="Reference odesílatele."
),
order_reference: str | None = Query(
default=None, alias="orderReference", description="Reference objednávky."
),
body: dict = Body(default={}, examples=[{"note": "Zrušeno zákazníkem"}]),
creds: Credentials = Depends(get_credentials),
):
"""`POST order/cancel` — identifikace přes customerReference nebo orderReference."""
params: dict[str, Any] = {}
if customer_reference:
params["customerReference"] = customer_reference
if order_reference:
params["orderReference"] = order_reference
resp = await cpl_request(
creds, "POST", "/order/cancel", params=params or None, json_body=body or {}
)
return relay_json(resp)