fix: validate date format in create_schedule with clear error message
This commit is contained in:
@@ -4,10 +4,17 @@ MCP Server for Naliia Module
|
|||||||
|
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from fastmcp import FastMCP
|
from fastmcp import FastMCP
|
||||||
from typing import List, AsyncIterator, Any, Dict
|
from typing import List, Dict, AsyncIterator, Any
|
||||||
import datetime
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
from tryton_mcp.config import settings
|
from tryton_mcp.config import settings
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||||
|
)
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
@@ -29,18 +36,63 @@ def tryton_call(name: str, args: list) -> Dict[str, Any]:
|
|||||||
return {"success": False, "error": str(e)}
|
return {"success": False, "error": str(e)}
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def find_customer_by_identifier(identifier: str) -> List[Dict]:
|
||||||
|
result = tryton_call(
|
||||||
|
"model.party.identifier.search_read",
|
||||||
|
[
|
||||||
|
[["code", "=", identifier]],
|
||||||
|
0,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
["party", "party.name"],
|
||||||
|
{"company": 1},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
if not result["success"]:
|
||||||
|
raise Exception(f"Customer with identifier {identifier} not found.")
|
||||||
|
|
||||||
|
return result["data"]
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def create_customer(
|
||||||
|
name: str, identifiers: List[tuple[str, List[Dict[str, Any]]]]
|
||||||
|
) -> List[int]:
|
||||||
|
|
||||||
|
result = tryton_call(
|
||||||
|
"model.party.party.create",
|
||||||
|
[{"name": name, "identifiers": identifiers}, {}],
|
||||||
|
)
|
||||||
|
|
||||||
|
if not result["success"]:
|
||||||
|
raise Exception(f"Failed to create {name} as customer")
|
||||||
|
|
||||||
|
return result["data"]
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
def create_schedule(
|
def create_schedule(
|
||||||
professional: int, description: str, customer: int, date: str, service_center: int
|
professional: int, description: str, customer: int, date: str, service_center: int
|
||||||
) -> List[int]:
|
) -> List[int]:
|
||||||
|
|
||||||
|
try:
|
||||||
|
dt = datetime.fromisoformat(date)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(
|
||||||
|
f"Invalid date format: '{date}'. Expected ISO format (e.g., '2026-03-15T10:00:00' or '2026-03-15T10:00:00+00:00')"
|
||||||
|
)
|
||||||
|
|
||||||
|
utc_dt = dt.astimezone(timezone.utc)
|
||||||
|
|
||||||
example_schedule = [
|
example_schedule = [
|
||||||
{
|
{
|
||||||
"professional": 6,
|
"professional": professional,
|
||||||
"description": description,
|
"description": description,
|
||||||
"customer": 4,
|
"customer": customer,
|
||||||
"date": datetime.datetime.fromisoformat(date),
|
"date": utc_dt,
|
||||||
"service_center": 11,
|
"service_center": service_center,
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
48
tests/test_customer.py
Normal file
48
tests/test_customer.py
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import pytest
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
import json
|
||||||
|
|
||||||
|
from tryton_mcp.server import create_customer
|
||||||
|
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent / "src"))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
class TestParty:
|
||||||
|
|
||||||
|
async def test_create_new_customer(self, mcp_client, mock_settings):
|
||||||
|
mock_client = mock_settings.get_client.return_value
|
||||||
|
mock_client.call.return_value = [1]
|
||||||
|
|
||||||
|
result = await mcp_client.call_tool(
|
||||||
|
"create_customer",
|
||||||
|
{
|
||||||
|
"name": "Alejandro Zapata",
|
||||||
|
"identifiers": [["create", [{"type": "mobile", "code": "310659595"}]]],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.content is not None
|
||||||
|
assert len(result.content) == 1
|
||||||
|
assert result.content[0].text == "[1]"
|
||||||
|
|
||||||
|
async def test_find_customer(self, mcp_client, mock_settings):
|
||||||
|
|
||||||
|
expected_return = [
|
||||||
|
{'id': 19, 'party': 44, 'party.': {'name': 'Alejandro Zapata', 'id': 44}}
|
||||||
|
]
|
||||||
|
|
||||||
|
mock_client = mock_settings.get_client.return_value
|
||||||
|
mock_client.call.return_value = expected_return
|
||||||
|
|
||||||
|
result = await mcp_client.call_tool(
|
||||||
|
"find_customer_by_identifier",
|
||||||
|
{
|
||||||
|
"identifier": "310659595"
|
||||||
|
})
|
||||||
|
|
||||||
|
assert result.content is not None
|
||||||
|
assert len(result.content) == 1
|
||||||
|
assert json.loads(result.content[0].text) == expected_return
|
||||||
@@ -2,6 +2,7 @@ import pytest
|
|||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
import datetime
|
import datetime
|
||||||
import json
|
import json
|
||||||
|
from fastmcp.exceptions import ToolError
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -78,6 +79,19 @@ class TestCreateSchedule:
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def test_create_schedule_invalid_date_format(self, mcp_client, mock_settings):
|
||||||
|
with pytest.raises(ToolError, match="Invalid date format"):
|
||||||
|
await mcp_client.call_tool(
|
||||||
|
"create_schedule",
|
||||||
|
{
|
||||||
|
"professional": 6,
|
||||||
|
"description": "Test appointment",
|
||||||
|
"customer": 4,
|
||||||
|
"date": "invalid-date",
|
||||||
|
"service_center": 11,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
class TestFindServiceCenters:
|
class TestFindServiceCenters:
|
||||||
|
|||||||
Reference in New Issue
Block a user