diff --git a/.env.example b/.env.example index f536008..258c733 100644 --- a/.env.example +++ b/.env.example @@ -15,3 +15,13 @@ DB_ROOT_PASSWORD=root # SMS.ir SMS_IR_API_KEY= SMS_IR_LINE_NUMBER=300000000000 + +# External API adapter +USE_EXTERNAL_API_MOCK=true +EXTERNAL_API_TIMEOUT=30 + +AI_SERVICE_BASE_URL=https://ai.example.com +AI_SERVICE_API_KEY= + +SENSOR_HUB_SERVICE_BASE_URL=https://sensor-hub.example.com +SENSOR_HUB_SERVICE_API_KEY= diff --git a/auth/views.py b/auth/views.py index e476131..e766ffc 100644 --- a/auth/views.py +++ b/auth/views.py @@ -1,6 +1,7 @@ import secrets -from django.contrib.auth import authenticate +from django.contrib.auth import authenticate, get_user_model +from django.db.models import Q from django.conf import settings from django.core.cache import cache from django.core.signing import BadSignature, SignatureExpired, TimestampSigner @@ -130,8 +131,21 @@ class LoginView(APIView): identifier = serializer.validated_data["identifier"] password = serializer.validated_data["password"] - user = authenticate(request, username=identifier, password=password) + User = get_user_model() + identifier = serializer.validated_data["identifier"] + password = serializer.validated_data["password"] + + user_obj = User.objects.filter( + Q(username=identifier) | Q(email=identifier) | Q(phone_number=identifier) + ).first() + + + + if user_obj: + user = authenticate(request, username=user_obj.username, password=password) + else: + user = None if user is None: return Response( {"code": 401, "msg": "Invalid credentials."}, diff --git a/config/settings.py b/config/settings.py index 97ea0fe..e1a82f5 100644 --- a/config/settings.py +++ b/config/settings.py @@ -34,6 +34,7 @@ INSTALLED_APPS = [ "irrigation_recommendation", "fertilization_recommendation", "farm_ai_assistant", + "external_api_adapter.apps.ExternalApiAdapterConfig", "rest_framework", "drf_spectacular", "drf_spectacular_sidecar", @@ -134,3 +135,17 @@ SMS_IR_API_KEY = os.environ.get("SMS_IR_API_KEY", "") SMS_IR_LINE_NUMBER = int(os.environ.get("SMS_IR_LINE_NUMBER", "300000000000")) CORS_ALLOW_ALL_ORIGINS = DEBUG + +USE_EXTERNAL_API_MOCK = os.getenv("USE_EXTERNAL_API_MOCK", "false").lower() == "true" +EXTERNAL_API_TIMEOUT = int(os.getenv("EXTERNAL_API_TIMEOUT", "30")) + +EXTERNAL_SERVICES = { + "ai": { + "base_url": os.getenv("AI_SERVICE_BASE_URL", ""), + "api_key": os.getenv("AI_SERVICE_API_KEY", ""), + }, + "sensor_hub": { + "base_url": os.getenv("SENSOR_HUB_SERVICE_BASE_URL", ""), + "api_key": os.getenv("SENSOR_HUB_SERVICE_API_KEY", ""), + }, +} diff --git a/dashboard/views.py b/dashboard/views.py index 15ab87a..0954b57 100644 --- a/dashboard/views.py +++ b/dashboard/views.py @@ -11,7 +11,8 @@ from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import extend_schema, extend_schema_view from config.swagger import code_response -from .mock_data import ALL_CARDS, CONFIG +from external_api_adapter import request as external_api_request +from .mock_data import CONFIG @extend_schema_view( @@ -58,4 +59,5 @@ class FarmDashboardCardsView(APIView): permission_classes = [] def get(self, request): - return Response({"code": 200, "msg": "OK", "data": ALL_CARDS}, status=status.HTTP_200_OK) + adapter_response = external_api_request("ai", "/dashboard-data/status", method="GET") + return Response(adapter_response.data, status=adapter_response.status_code) diff --git a/docker-compose.yaml b/docker-compose.yaml index ffde23f..29ff35c 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,7 +1,7 @@ # Development: volumes mount source so code updates apply without rebuild services: db: - image: docker-mirror.liara.ir/mysql:8.0 + image: mysql:8.0 environment: MYSQL_DATABASE: ${DB_NAME:-croplogic} MYSQL_USER: ${DB_USER:-croplogic} @@ -16,7 +16,7 @@ services: retries: 5 phpmyadmin: - image: docker-mirror.liara.ir/phpmyadmin:latest + image: phpmyadmin:latest environment: PMA_HOST: db PMA_PORT: 3306 diff --git a/external_api_adapter/README.md b/external_api_adapter/README.md new file mode 100644 index 0000000..cd8270b --- /dev/null +++ b/external_api_adapter/README.md @@ -0,0 +1,33 @@ +# External API Adapter + +## Settings + +```python +USE_EXTERNAL_API_MOCK = os.getenv("USE_EXTERNAL_API_MOCK", "false").lower() == "true" + +EXTERNAL_SERVICES = { + "ai": { + "base_url": os.getenv("AI_SERVICE_BASE_URL", ""), + "api_key": os.getenv("AI_SERVICE_API_KEY", ""), + }, + "sensor_hub": { + "base_url": os.getenv("SENSOR_HUB_SERVICE_BASE_URL", ""), + "api_key": os.getenv("SENSOR_HUB_SERVICE_API_KEY", ""), + }, +} +``` + +## Usage + +```python +from rest_framework.response import Response +from rest_framework.views import APIView + +from external_api_adapter import request + + +class PredictionProxyView(APIView): + def get(self, request_obj): + adapter_response = request("ai", "/predict") + return Response(adapter_response.data, status=adapter_response.status_code) +``` diff --git a/external_api_adapter/__init__.py b/external_api_adapter/__init__.py new file mode 100644 index 0000000..7270034 --- /dev/null +++ b/external_api_adapter/__init__.py @@ -0,0 +1,3 @@ +from .adapter import ExternalAPIAdapter, request + +__all__ = ["ExternalAPIAdapter", "request"] diff --git a/external_api_adapter/adapter.py b/external_api_adapter/adapter.py new file mode 100644 index 0000000..6fade40 --- /dev/null +++ b/external_api_adapter/adapter.py @@ -0,0 +1,103 @@ +from dataclasses import dataclass, field + +import requests +from django.conf import settings + +from .exceptions import ExternalAPIRequestError +from .mock_loader import MockLoader +from .services import ServiceRegistry + + +@dataclass +class AdapterResponse: + status_code: int + data: object + headers: dict = field(default_factory=dict) + is_mock: bool = False + + +class ExternalAPIAdapter: + def __init__(self, service_registry=None, mock_loader=None): + self.service_registry = service_registry or ServiceRegistry() + self.mock_loader = mock_loader or MockLoader() + + def request(self, service_name, path, method="GET", payload=None, query=None, headers=None): + request_method = method.upper() + self._validate_method(request_method) + service = self.service_registry.get(service_name) + + if getattr(settings, "USE_EXTERNAL_API_MOCK", False): + mock_response = self.mock_loader.load(service_name=service_name, path=path, method=request_method) + return AdapterResponse( + status_code=mock_response.status_code, + data=mock_response.data, + headers={"X-Mock-File": mock_response.file_path}, + is_mock=True, + ) + + return self._call_real_api( + service=service, + path=path, + method=request_method, + payload=payload, + query=query, + headers=headers, + ) + + def _call_real_api(self, service, path, method, payload=None, query=None, headers=None): + base_url = service.get("base_url", "").rstrip("/") + api_key = service.get("api_key", "") + if not base_url: + raise ExternalAPIRequestError("External service base_url is not configured.") + url = f"{base_url}/{str(path).lstrip('/')}" + + request_headers = { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json", + } + if headers: + request_headers.update(headers) + + try: + response = requests.request( + method=method, + url=url, + json=payload, + params=query, + headers=request_headers, + timeout=getattr(settings, "EXTERNAL_API_TIMEOUT", 30), + ) + except requests.RequestException as exc: + raise ExternalAPIRequestError(f"External API request failed for '{url}': {exc}") from exc + + try: + response_data = response.json() + except ValueError: + response_data = response.text + + return AdapterResponse( + status_code=response.status_code, + data=response_data, + headers=dict(response.headers), + is_mock=False, + ) + + @staticmethod + def _validate_method(method): + supported_methods = {"GET", "POST", "PUT", "DELETE"} + if method not in supported_methods: + raise ValueError(f"Unsupported HTTP method '{method}'. Supported methods: {sorted(supported_methods)}") + + +_default_adapter = ExternalAPIAdapter() + + +def request(service_name, path, method="GET", payload=None, query=None, headers=None): + return _default_adapter.request( + service_name=service_name, + path=path, + method=method, + payload=payload, + query=query, + headers=headers, + ) diff --git a/external_api_adapter/apps.py b/external_api_adapter/apps.py new file mode 100644 index 0000000..f1d6802 --- /dev/null +++ b/external_api_adapter/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class ExternalApiAdapterConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "external_api_adapter" + verbose_name = "External API Adapter" diff --git a/external_api_adapter/exceptions.py b/external_api_adapter/exceptions.py new file mode 100644 index 0000000..3a5b109 --- /dev/null +++ b/external_api_adapter/exceptions.py @@ -0,0 +1,18 @@ +class ExternalAPIAdapterError(Exception): + pass + + +class ServiceNotFound(ExternalAPIAdapterError): + pass + + +class MockDirectoryNotFound(ExternalAPIAdapterError): + pass + + +class MockFileNotFound(ExternalAPIAdapterError): + pass + + +class ExternalAPIRequestError(ExternalAPIAdapterError): + pass diff --git a/external_api_adapter/json/ai/dashboard-data/generate/post_202.json b/external_api_adapter/json/ai/dashboard-data/generate/post_202.json new file mode 100644 index 0000000..34b0f71 --- /dev/null +++ b/external_api_adapter/json/ai/dashboard-data/generate/post_202.json @@ -0,0 +1,8 @@ +{ + "code": 202, + "msg": "dashboard task queued", + "data": { + "task_id": "dashboard-task-123", + "status_url": "/api/dashboard-data/dashboard-task-123/status/" + } +} diff --git a/external_api_adapter/json/ai/dashboard-data/generate/post_400.json b/external_api_adapter/json/ai/dashboard-data/generate/post_400.json new file mode 100644 index 0000000..5df03b8 --- /dev/null +++ b/external_api_adapter/json/ai/dashboard-data/generate/post_400.json @@ -0,0 +1,5 @@ +{ + "code": 400, + "msg": "پارامتر sensor_id الزامی است.", + "data": null +} diff --git a/external_api_adapter/json/ai/dashboard-data/status/get_200_failure.json b/external_api_adapter/json/ai/dashboard-data/status/get_200_failure.json new file mode 100644 index 0000000..bc4f2eb --- /dev/null +++ b/external_api_adapter/json/ai/dashboard-data/status/get_200_failure.json @@ -0,0 +1,9 @@ +{ + "code": 200, + "msg": "success", + "data": { + "task_id": "dashboard-task-123", + "status": "FAILURE", + "error": "خطا در ساخت کارت‌های داشبورد." + } +} diff --git a/external_api_adapter/json/ai/dashboard-data/status/get_200_pending.json b/external_api_adapter/json/ai/dashboard-data/status/get_200_pending.json new file mode 100644 index 0000000..74dafc6 --- /dev/null +++ b/external_api_adapter/json/ai/dashboard-data/status/get_200_pending.json @@ -0,0 +1,9 @@ +{ + "code": 200, + "msg": "success", + "data": { + "task_id": "dashboard-task-123", + "status": "PENDING", + "message": "تسک در صف یا یافت نشد." + } +} diff --git a/external_api_adapter/json/ai/dashboard-data/status/get_200_progress.json b/external_api_adapter/json/ai/dashboard-data/status/get_200_progress.json new file mode 100644 index 0000000..e3515e5 --- /dev/null +++ b/external_api_adapter/json/ai/dashboard-data/status/get_200_progress.json @@ -0,0 +1,14 @@ +{ + "code": 200, + "msg": "success", + "data": { + "task_id": "dashboard-task-123", + "status": "PROGRESS", + "progress": { + "current": 5, + "total": 15, + "card": "sensorValuesList", + "message": "processing sensorValuesList" + } + } +} diff --git a/external_api_adapter/json/ai/dashboard-data/status/get_200_success.json b/external_api_adapter/json/ai/dashboard-data/status/get_200_success.json new file mode 100644 index 0000000..4de043c --- /dev/null +++ b/external_api_adapter/json/ai/dashboard-data/status/get_200_success.json @@ -0,0 +1,41 @@ +{ + "code": 200, + "msg": "success", + "data": { + "task_id": "dashboard-task-123", + "status": "SUCCESS", + "result": { + "sensor_id": "550e8400-e29b-41d4-a716-446655440000", + "all_cards": { + "farmOverviewKpis": { + "healthScore": 82, + "activeAlerts": 2, + "waterNeedMm": 18.4 + }, + "sensorValuesList": { + "items": [ + { + "label": "رطوبت خاک", + "value": 45.2, + "unit": "%" + }, + { + "label": "دما خاک", + "value": 22.5, + "unit": "°C" + } + ] + }, + "recommendationsList": { + "items": [ + { + "recommendation_title": "تنظیم نوبت آبیاری", + "suggested_action": "آبیاری بعدی را صبح فردا انجام دهید.", + "urgency_level": "high" + } + ] + } + } + } + } +} diff --git a/external_api_adapter/json/ai/fertilization/recommend/post_202.json b/external_api_adapter/json/ai/fertilization/recommend/post_202.json new file mode 100644 index 0000000..e8dc23f --- /dev/null +++ b/external_api_adapter/json/ai/fertilization/recommend/post_202.json @@ -0,0 +1,8 @@ +{ + "code": 202, + "msg": "تسک توصیه کودهی در صف قرار گرفت.", + "data": { + "task_id": "fert-task-123", + "status_url": "/api/fertilization/recommend/fert-task-123/status/" + } +} diff --git a/external_api_adapter/json/ai/fertilization/recommend/post_400.json b/external_api_adapter/json/ai/fertilization/recommend/post_400.json new file mode 100644 index 0000000..9fdc597 --- /dev/null +++ b/external_api_adapter/json/ai/fertilization/recommend/post_400.json @@ -0,0 +1,9 @@ +{ + "code": 400, + "msg": "داده نامعتبر.", + "data": { + "sensor_uuid": [ + "This field is required." + ] + } +} diff --git a/external_api_adapter/json/ai/fertilization/status/get_200_failure.json b/external_api_adapter/json/ai/fertilization/status/get_200_failure.json new file mode 100644 index 0000000..fb8bad0 --- /dev/null +++ b/external_api_adapter/json/ai/fertilization/status/get_200_failure.json @@ -0,0 +1,9 @@ +{ + "code": 200, + "msg": "success", + "data": { + "task_id": "fert-task-123", + "status": "FAILURE", + "error": "خطا در دریافت توصیه کودهی." + } +} diff --git a/external_api_adapter/json/ai/fertilization/status/get_200_pending.json b/external_api_adapter/json/ai/fertilization/status/get_200_pending.json new file mode 100644 index 0000000..110be1a --- /dev/null +++ b/external_api_adapter/json/ai/fertilization/status/get_200_pending.json @@ -0,0 +1,9 @@ +{ + "code": 200, + "msg": "success", + "data": { + "task_id": "fert-task-123", + "status": "PENDING", + "message": "تسک در صف یا یافت نشد." + } +} diff --git a/external_api_adapter/json/ai/fertilization/status/get_200_progress.json b/external_api_adapter/json/ai/fertilization/status/get_200_progress.json new file mode 100644 index 0000000..ffdf909 --- /dev/null +++ b/external_api_adapter/json/ai/fertilization/status/get_200_progress.json @@ -0,0 +1,11 @@ +{ + "code": 200, + "msg": "success", + "data": { + "task_id": "fert-task-123", + "status": "PROGRESS", + "progress": { + "message": "در حال پردازش توصیه کودهی..." + } + } +} diff --git a/external_api_adapter/json/ai/fertilization/status/get_200_success.json b/external_api_adapter/json/ai/fertilization/status/get_200_success.json new file mode 100644 index 0000000..7b46ba1 --- /dev/null +++ b/external_api_adapter/json/ai/fertilization/status/get_200_success.json @@ -0,0 +1,19 @@ +{ + "code": 200, + "msg": "success", + "data": { + "task_id": "fert-task-123", + "status": "SUCCESS", + "result": { + "plan": { + "npkRatio": "20-20-20", + "amountPerHectare": "150 kg/ha", + "applicationMethod": "کودآبیاری در دو نوبت", + "applicationInterval": "هر ۱۰ روز", + "reasoning": "نیتروژن و پتاسیم خاک در محدوده متوسط است و گیاه در فاز رویشی نیاز تغذیه‌ای بالاتری دارد." + }, + "raw_response": "{\"plan\":{\"npkRatio\":\"20-20-20\"}}", + "status": "completed" + } + } +} diff --git a/external_api_adapter/json/ai/index.json b/external_api_adapter/json/ai/index.json new file mode 100644 index 0000000..d051fb6 --- /dev/null +++ b/external_api_adapter/json/ai/index.json @@ -0,0 +1,604 @@ +[ + { + "method": "POST", + "path": "/api/dashboard-data/generate/", + "status_code": 202, + "description": "Dashboard data task queued", + "file": "json/mock_data/dashboard-data/generate/post_202.json" + }, + { + "method": "POST", + "path": "/api/dashboard-data/generate/", + "status_code": 400, + "description": "Missing sensor_id", + "file": "json/mock_data/dashboard-data/generate/post_400.json" + }, + { + "method": "GET", + "path": "/api/dashboard-data/{task_id}/status/", + "status_code": 200, + "description": "Pending dashboard task", + "file": "json/mock_data/dashboard-data/status/get_200_pending.json" + }, + { + "method": "GET", + "path": "/api/dashboard-data/{task_id}/status/", + "status_code": 200, + "description": "Dashboard task in progress", + "file": "json/mock_data/dashboard-data/status/get_200_progress.json" + }, + { + "method": "GET", + "path": "/api/dashboard-data/{task_id}/status/", + "status_code": 200, + "description": "Successful dashboard task", + "file": "json/mock_data/dashboard-data/status/get_200_success.json" + }, + { + "method": "GET", + "path": "/api/dashboard-data/{task_id}/status/", + "status_code": 200, + "description": "Failed dashboard task", + "file": "json/mock_data/dashboard-data/status/get_200_failure.json" + }, + { + "method": "POST", + "path": "/api/fertilization/recommend/", + "status_code": 202, + "description": "Fertilization task queued", + "file": "json/mock_data/fertilization/recommend/post_202.json" + }, + { + "method": "POST", + "path": "/api/fertilization/recommend/", + "status_code": 400, + "description": "Validation error", + "file": "json/mock_data/fertilization/recommend/post_400.json" + }, + { + "method": "GET", + "path": "/api/fertilization/recommend/{task_id}/status/", + "status_code": 200, + "description": "Fertilization status pending", + "file": "json/mock_data/fertilization/status/get_200_pending.json" + }, + { + "method": "GET", + "path": "/api/fertilization/recommend/{task_id}/status/", + "status_code": 200, + "description": "Fertilization status progress", + "file": "json/mock_data/fertilization/status/get_200_progress.json" + }, + { + "method": "GET", + "path": "/api/fertilization/recommend/{task_id}/status/", + "status_code": 200, + "description": "Fertilization status success", + "file": "json/mock_data/fertilization/status/get_200_success.json" + }, + { + "method": "GET", + "path": "/api/fertilization/recommend/{task_id}/status/", + "status_code": 200, + "description": "Fertilization status failure", + "file": "json/mock_data/fertilization/status/get_200_failure.json" + }, + { + "method": "GET", + "path": "/api/irrigation/", + "status_code": 200, + "description": "List irrigation methods", + "file": "json/mock_data/irrigation/methods/get_200.json" + }, + { + "method": "POST", + "path": "/api/irrigation/", + "status_code": 201, + "description": "Create irrigation method", + "file": "json/mock_data/irrigation/methods/post_201.json" + }, + { + "method": "POST", + "path": "/api/irrigation/", + "status_code": 400, + "description": "Irrigation create validation error", + "file": "json/mock_data/irrigation/methods/post_400.json" + }, + { + "method": "POST", + "path": "/api/irrigation/recommend/", + "status_code": 202, + "description": "Irrigation recommendation task queued", + "file": "json/mock_data/irrigation/recommend/post_202.json" + }, + { + "method": "POST", + "path": "/api/irrigation/recommend/", + "status_code": 400, + "description": "Irrigation recommendation validation error", + "file": "json/mock_data/irrigation/recommend/post_400.json" + }, + { + "method": "GET", + "path": "/api/irrigation/recommend/{task_id}/status/", + "status_code": 200, + "description": "Irrigation recommendation status pending", + "file": "json/mock_data/irrigation/recommend/status/get_200_pending.json" + }, + { + "method": "GET", + "path": "/api/irrigation/recommend/{task_id}/status/", + "status_code": 200, + "description": "Irrigation recommendation status progress", + "file": "json/mock_data/irrigation/recommend/status/get_200_progress.json" + }, + { + "method": "GET", + "path": "/api/irrigation/recommend/{task_id}/status/", + "status_code": 200, + "description": "Irrigation recommendation status success", + "file": "json/mock_data/irrigation/recommend/status/get_200_success.json" + }, + { + "method": "GET", + "path": "/api/irrigation/recommend/{task_id}/status/", + "status_code": 200, + "description": "Irrigation recommendation status failure", + "file": "json/mock_data/irrigation/recommend/status/get_200_failure.json" + }, + { + "method": "GET", + "path": "/api/irrigation/{pk}/", + "status_code": 200, + "description": "Irrigation method get success", + "file": "json/mock_data/irrigation/method-detail/get_200.json" + }, + { + "method": "GET", + "path": "/api/irrigation/{pk}/", + "status_code": 404, + "description": "Irrigation method get not found", + "file": "json/mock_data/irrigation/method-detail/get_404.json" + }, + { + "method": "PUT", + "path": "/api/irrigation/{pk}/", + "status_code": 200, + "description": "Irrigation method put success", + "file": "json/mock_data/irrigation/method-detail/put_200.json" + }, + { + "method": "PUT", + "path": "/api/irrigation/{pk}/", + "status_code": 400, + "description": "Irrigation method put validation error", + "file": "json/mock_data/irrigation/method-detail/put_400.json" + }, + { + "method": "PUT", + "path": "/api/irrigation/{pk}/", + "status_code": 404, + "description": "Irrigation method put not found", + "file": "json/mock_data/irrigation/method-detail/put_404.json" + }, + { + "method": "PATCH", + "path": "/api/irrigation/{pk}/", + "status_code": 200, + "description": "Irrigation method patch success", + "file": "json/mock_data/irrigation/method-detail/patch_200.json" + }, + { + "method": "PATCH", + "path": "/api/irrigation/{pk}/", + "status_code": 400, + "description": "Irrigation method patch validation error", + "file": "json/mock_data/irrigation/method-detail/patch_400.json" + }, + { + "method": "PATCH", + "path": "/api/irrigation/{pk}/", + "status_code": 404, + "description": "Irrigation method patch not found", + "file": "json/mock_data/irrigation/method-detail/patch_404.json" + }, + { + "method": "DELETE", + "path": "/api/irrigation/{pk}/", + "status_code": 200, + "description": "Delete irrigation method", + "file": "json/mock_data/irrigation/method-detail/delete_200.json" + }, + { + "method": "DELETE", + "path": "/api/irrigation/{pk}/", + "status_code": 404, + "description": "Delete irrigation method not found", + "file": "json/mock_data/irrigation/method-detail/delete_404.json" + }, + { + "method": "GET", + "path": "/api/soil-data/", + "status_code": 200, + "description": "Soil data served from database", + "file": "json/mock_data/soil-data/get_200_database.json" + }, + { + "method": "GET", + "path": "/api/soil-data/", + "status_code": 202, + "description": "Soil data fetch task queued", + "file": "json/mock_data/soil-data/get_202_queued.json" + }, + { + "method": "GET", + "path": "/api/soil-data/", + "status_code": 400, + "description": "Soil data validation error", + "file": "json/mock_data/soil-data/get_400.json" + }, + { + "method": "POST", + "path": "/api/soil-data/", + "status_code": 200, + "description": "Soil data POST served from database", + "file": "json/mock_data/soil-data/post_200_database.json" + }, + { + "method": "POST", + "path": "/api/soil-data/", + "status_code": 202, + "description": "Soil data POST task queued", + "file": "json/mock_data/soil-data/post_202_queued.json" + }, + { + "method": "POST", + "path": "/api/soil-data/", + "status_code": 400, + "description": "Soil data POST validation error", + "file": "json/mock_data/soil-data/post_400.json" + }, + { + "method": "GET", + "path": "/api/soil-data/tasks/{task_id}/status/", + "status_code": 200, + "description": "Soil task status pending", + "file": "json/mock_data/soil-data/status/get_200_pending.json" + }, + { + "method": "GET", + "path": "/api/soil-data/tasks/{task_id}/status/", + "status_code": 200, + "description": "Soil task status progress", + "file": "json/mock_data/soil-data/status/get_200_progress.json" + }, + { + "method": "GET", + "path": "/api/soil-data/tasks/{task_id}/status/", + "status_code": 200, + "description": "Soil task status success", + "file": "json/mock_data/soil-data/status/get_200_success.json" + }, + { + "method": "GET", + "path": "/api/soil-data/tasks/{task_id}/status/", + "status_code": 200, + "description": "Soil task status failure", + "file": "json/mock_data/soil-data/status/get_200_failure.json" + }, + { + "method": "GET", + "path": "/api/plants/", + "status_code": 200, + "description": "List plants", + "file": "json/mock_data/plant/list-get_200.json" + }, + { + "method": "POST", + "path": "/api/plants/", + "status_code": 201, + "description": "Create plant", + "file": "json/mock_data/plant/create-post_201.json" + }, + { + "method": "POST", + "path": "/api/plants/", + "status_code": 400, + "description": "Plant create validation error", + "file": "json/mock_data/plant/create-post_400.json" + }, + { + "method": "GET", + "path": "/api/plants/{pk}/", + "status_code": 200, + "description": "Plant detail get success", + "file": "json/mock_data/plant/detail-get_200.json" + }, + { + "method": "GET", + "path": "/api/plants/{pk}/", + "status_code": 404, + "description": "Plant detail get not found", + "file": "json/mock_data/plant/detail-get_404.json" + }, + { + "method": "PUT", + "path": "/api/plants/{pk}/", + "status_code": 200, + "description": "Plant detail put success", + "file": "json/mock_data/plant/detail-put_200.json" + }, + { + "method": "PUT", + "path": "/api/plants/{pk}/", + "status_code": 400, + "description": "Plant detail put validation error", + "file": "json/mock_data/plant/detail-put_400.json" + }, + { + "method": "PUT", + "path": "/api/plants/{pk}/", + "status_code": 404, + "description": "Plant detail put not found", + "file": "json/mock_data/plant/detail-put_404.json" + }, + { + "method": "PATCH", + "path": "/api/plants/{pk}/", + "status_code": 200, + "description": "Plant detail patch success", + "file": "json/mock_data/plant/detail-patch_200.json" + }, + { + "method": "PATCH", + "path": "/api/plants/{pk}/", + "status_code": 400, + "description": "Plant detail patch validation error", + "file": "json/mock_data/plant/detail-patch_400.json" + }, + { + "method": "PATCH", + "path": "/api/plants/{pk}/", + "status_code": 404, + "description": "Plant detail patch not found", + "file": "json/mock_data/plant/detail-patch_404.json" + }, + { + "method": "DELETE", + "path": "/api/plants/{pk}/", + "status_code": 200, + "description": "Delete plant success", + "file": "json/mock_data/plant/detail-delete_200.json" + }, + { + "method": "DELETE", + "path": "/api/plants/{pk}/", + "status_code": 404, + "description": "Delete plant not found", + "file": "json/mock_data/plant/detail-delete_404.json" + }, + { + "method": "POST", + "path": "/api/plants/fetch-info/", + "status_code": 200, + "description": "Fetch plant info success", + "file": "json/mock_data/plant/fetch-info-post_200.json" + }, + { + "method": "POST", + "path": "/api/plants/fetch-info/", + "status_code": 400, + "description": "Fetch plant info missing name", + "file": "json/mock_data/plant/fetch-info-post_400.json" + }, + { + "method": "POST", + "path": "/api/plants/fetch-info/", + "status_code": 503, + "description": "Fetch plant info service unavailable", + "file": "json/mock_data/plant/fetch-info-post_503.json" + }, + { + "method": "POST", + "path": "/api/rag/chat/", + "status_code": 200, + "description": "RAG chat streaming response", + "file": "json/mock_data/rag/chat-post_200_stream.json" + }, + { + "method": "POST", + "path": "/api/rag/chat/", + "status_code": 400, + "description": "Missing query", + "file": "json/mock_data/rag/chat-post_400_missing_query.json" + }, + { + "method": "POST", + "path": "/api/rag/chat/", + "status_code": 400, + "description": "Invalid service id", + "file": "json/mock_data/rag/chat-post_400_invalid_service.json" + }, + { + "method": "POST", + "path": "/api/rag/chat/", + "status_code": 400, + "description": "Missing user_id for service", + "file": "json/mock_data/rag/chat-post_400_missing_user.json" + }, + { + "method": "POST", + "path": "/api/rag/recommend/irrigation/", + "status_code": 202, + "description": "RAG irrigation task queued", + "file": "json/mock_data/rag/irrigation/post_202.json" + }, + { + "method": "POST", + "path": "/api/rag/recommend/irrigation/", + "status_code": 400, + "description": "RAG irrigation validation error", + "file": "json/mock_data/rag/irrigation/post_400.json" + }, + { + "method": "GET", + "path": "/api/rag/recommend/irrigation/{task_id}/status/", + "status_code": 200, + "description": "RAG irrigation status pending", + "file": "json/mock_data/rag/irrigation/status/get_200_pending.json" + }, + { + "method": "GET", + "path": "/api/rag/recommend/irrigation/{task_id}/status/", + "status_code": 200, + "description": "RAG irrigation status progress", + "file": "json/mock_data/rag/irrigation/status/get_200_progress.json" + }, + { + "method": "GET", + "path": "/api/rag/recommend/irrigation/{task_id}/status/", + "status_code": 200, + "description": "RAG irrigation status success", + "file": "json/mock_data/rag/irrigation/status/get_200_success.json" + }, + { + "method": "GET", + "path": "/api/rag/recommend/irrigation/{task_id}/status/", + "status_code": 200, + "description": "RAG irrigation status failure", + "file": "json/mock_data/rag/irrigation/status/get_200_failure.json" + }, + { + "method": "POST", + "path": "/api/rag/recommend/fertilization/", + "status_code": 202, + "description": "RAG fertilization task queued", + "file": "json/mock_data/rag/fertilization/post_202.json" + }, + { + "method": "POST", + "path": "/api/rag/recommend/fertilization/", + "status_code": 400, + "description": "RAG fertilization validation error", + "file": "json/mock_data/rag/fertilization/post_400.json" + }, + { + "method": "GET", + "path": "/api/rag/recommend/fertilization/{task_id}/status/", + "status_code": 200, + "description": "RAG fertilization status pending", + "file": "json/mock_data/rag/fertilization/status/get_200_pending.json" + }, + { + "method": "GET", + "path": "/api/rag/recommend/fertilization/{task_id}/status/", + "status_code": 200, + "description": "RAG fertilization status progress", + "file": "json/mock_data/rag/fertilization/status/get_200_progress.json" + }, + { + "method": "GET", + "path": "/api/rag/recommend/fertilization/{task_id}/status/", + "status_code": 200, + "description": "RAG fertilization status success", + "file": "json/mock_data/rag/fertilization/status/get_200_success.json" + }, + { + "method": "GET", + "path": "/api/rag/recommend/fertilization/{task_id}/status/", + "status_code": 200, + "description": "RAG fertilization status failure", + "file": "json/mock_data/rag/fertilization/status/get_200_failure.json" + }, + { + "method": "PUT", + "path": "/api/sensor-data/{uuid_sensor}/", + "status_code": 200, + "description": "Sensor update put success", + "file": "json/mock_data/sensor-data/update-put_200.json" + }, + { + "method": "PUT", + "path": "/api/sensor-data/{uuid_sensor}/", + "status_code": 400, + "description": "Sensor update put validation error", + "file": "json/mock_data/sensor-data/update-put_400.json" + }, + { + "method": "PUT", + "path": "/api/sensor-data/{uuid_sensor}/", + "status_code": 404, + "description": "Sensor update put location not found", + "file": "json/mock_data/sensor-data/update-put_404.json" + }, + { + "method": "PATCH", + "path": "/api/sensor-data/{uuid_sensor}/", + "status_code": 200, + "description": "Sensor update patch success", + "file": "json/mock_data/sensor-data/update-patch_200.json" + }, + { + "method": "PATCH", + "path": "/api/sensor-data/{uuid_sensor}/", + "status_code": 400, + "description": "Sensor update patch validation error", + "file": "json/mock_data/sensor-data/update-patch_400.json" + }, + { + "method": "PATCH", + "path": "/api/sensor-data/{uuid_sensor}/", + "status_code": 404, + "description": "Sensor update patch location not found", + "file": "json/mock_data/sensor-data/update-patch_404.json" + }, + { + "method": "POST", + "path": "/api/sensor-data/parameters/", + "status_code": 201, + "description": "Create sensor parameter", + "file": "json/mock_data/sensor-data/parameters-post_201.json" + }, + { + "method": "POST", + "path": "/api/sensor-data/parameters/", + "status_code": 400, + "description": "Sensor parameter validation error", + "file": "json/mock_data/sensor-data/parameters-post_400.json" + }, + { + "method": "POST", + "path": "/api/tasks/", + "status_code": 200, + "description": "Task trigger success", + "file": "json/mock_data/tasks/post_200.json" + }, + { + "method": "GET", + "path": "/api/tasks/{task_id}/status/", + "status_code": 200, + "description": "Task status pending", + "file": "json/mock_data/tasks/status/get_200_pending.json" + }, + { + "method": "GET", + "path": "/api/tasks/{task_id}/status/", + "status_code": 200, + "description": "Task status progress", + "file": "json/mock_data/tasks/status/get_200_progress.json" + }, + { + "method": "GET", + "path": "/api/tasks/{task_id}/status/", + "status_code": 200, + "description": "Task status success", + "file": "json/mock_data/tasks/status/get_200_success.json" + }, + { + "method": "GET", + "path": "/api/tasks/{task_id}/status/", + "status_code": 200, + "description": "Task status failure", + "file": "json/mock_data/tasks/status/get_200_failure.json" + } +] diff --git a/external_api_adapter/json/ai/irrigation/method-detail/delete_200.json b/external_api_adapter/json/ai/irrigation/method-detail/delete_200.json new file mode 100644 index 0000000..ed52092 --- /dev/null +++ b/external_api_adapter/json/ai/irrigation/method-detail/delete_200.json @@ -0,0 +1,5 @@ +{ + "code": 200, + "msg": "روش آبیاری با موفقیت حذف شد.", + "data": null +} diff --git a/external_api_adapter/json/ai/irrigation/method-detail/delete_404.json b/external_api_adapter/json/ai/irrigation/method-detail/delete_404.json new file mode 100644 index 0000000..54dcfff --- /dev/null +++ b/external_api_adapter/json/ai/irrigation/method-detail/delete_404.json @@ -0,0 +1,5 @@ +{ + "code": 404, + "msg": "روش آبیاری یافت نشد.", + "data": null +} diff --git a/external_api_adapter/json/ai/irrigation/method-detail/get_200.json b/external_api_adapter/json/ai/irrigation/method-detail/get_200.json new file mode 100644 index 0000000..5988c67 --- /dev/null +++ b/external_api_adapter/json/ai/irrigation/method-detail/get_200.json @@ -0,0 +1,18 @@ +{ + "code": 200, + "msg": "success", + "data": { + "id": 1, + "name": "آبیاری قطره‌ای", + "category": "موضعی", + "description": "آبیاری با دبی کم و راندمان بالا", + "water_efficiency_percent": 90.0, + "water_pressure_required": "۱-۲ اتمسفر", + "flow_rate": "۲-۸ لیتر در ساعت", + "coverage_area": "بسته به طراحی سیستم", + "soil_type": "اکثر خاک‌ها", + "climate_suitability": "گرم و خشک", + "created_at": "2025-03-20T10:00:00Z", + "updated_at": "2025-03-24T10:00:00Z" + } +} diff --git a/external_api_adapter/json/ai/irrigation/method-detail/get_404.json b/external_api_adapter/json/ai/irrigation/method-detail/get_404.json new file mode 100644 index 0000000..54dcfff --- /dev/null +++ b/external_api_adapter/json/ai/irrigation/method-detail/get_404.json @@ -0,0 +1,5 @@ +{ + "code": 404, + "msg": "روش آبیاری یافت نشد.", + "data": null +} diff --git a/external_api_adapter/json/ai/irrigation/method-detail/patch_200.json b/external_api_adapter/json/ai/irrigation/method-detail/patch_200.json new file mode 100644 index 0000000..5988c67 --- /dev/null +++ b/external_api_adapter/json/ai/irrigation/method-detail/patch_200.json @@ -0,0 +1,18 @@ +{ + "code": 200, + "msg": "success", + "data": { + "id": 1, + "name": "آبیاری قطره‌ای", + "category": "موضعی", + "description": "آبیاری با دبی کم و راندمان بالا", + "water_efficiency_percent": 90.0, + "water_pressure_required": "۱-۲ اتمسفر", + "flow_rate": "۲-۸ لیتر در ساعت", + "coverage_area": "بسته به طراحی سیستم", + "soil_type": "اکثر خاک‌ها", + "climate_suitability": "گرم و خشک", + "created_at": "2025-03-20T10:00:00Z", + "updated_at": "2025-03-24T10:00:00Z" + } +} diff --git a/external_api_adapter/json/ai/irrigation/method-detail/patch_400.json b/external_api_adapter/json/ai/irrigation/method-detail/patch_400.json new file mode 100644 index 0000000..2e4dd22 --- /dev/null +++ b/external_api_adapter/json/ai/irrigation/method-detail/patch_400.json @@ -0,0 +1,9 @@ +{ + "code": 400, + "msg": "داده نامعتبر.", + "data": { + "name": [ + "This field may not be blank." + ] + } +} diff --git a/external_api_adapter/json/ai/irrigation/method-detail/patch_404.json b/external_api_adapter/json/ai/irrigation/method-detail/patch_404.json new file mode 100644 index 0000000..54dcfff --- /dev/null +++ b/external_api_adapter/json/ai/irrigation/method-detail/patch_404.json @@ -0,0 +1,5 @@ +{ + "code": 404, + "msg": "روش آبیاری یافت نشد.", + "data": null +} diff --git a/external_api_adapter/json/ai/irrigation/method-detail/put_200.json b/external_api_adapter/json/ai/irrigation/method-detail/put_200.json new file mode 100644 index 0000000..5988c67 --- /dev/null +++ b/external_api_adapter/json/ai/irrigation/method-detail/put_200.json @@ -0,0 +1,18 @@ +{ + "code": 200, + "msg": "success", + "data": { + "id": 1, + "name": "آبیاری قطره‌ای", + "category": "موضعی", + "description": "آبیاری با دبی کم و راندمان بالا", + "water_efficiency_percent": 90.0, + "water_pressure_required": "۱-۲ اتمسفر", + "flow_rate": "۲-۸ لیتر در ساعت", + "coverage_area": "بسته به طراحی سیستم", + "soil_type": "اکثر خاک‌ها", + "climate_suitability": "گرم و خشک", + "created_at": "2025-03-20T10:00:00Z", + "updated_at": "2025-03-24T10:00:00Z" + } +} diff --git a/external_api_adapter/json/ai/irrigation/method-detail/put_400.json b/external_api_adapter/json/ai/irrigation/method-detail/put_400.json new file mode 100644 index 0000000..2e4dd22 --- /dev/null +++ b/external_api_adapter/json/ai/irrigation/method-detail/put_400.json @@ -0,0 +1,9 @@ +{ + "code": 400, + "msg": "داده نامعتبر.", + "data": { + "name": [ + "This field may not be blank." + ] + } +} diff --git a/external_api_adapter/json/ai/irrigation/method-detail/put_404.json b/external_api_adapter/json/ai/irrigation/method-detail/put_404.json new file mode 100644 index 0000000..54dcfff --- /dev/null +++ b/external_api_adapter/json/ai/irrigation/method-detail/put_404.json @@ -0,0 +1,5 @@ +{ + "code": 404, + "msg": "روش آبیاری یافت نشد.", + "data": null +} diff --git a/external_api_adapter/json/ai/irrigation/methods/get_200.json b/external_api_adapter/json/ai/irrigation/methods/get_200.json new file mode 100644 index 0000000..a2e511e --- /dev/null +++ b/external_api_adapter/json/ai/irrigation/methods/get_200.json @@ -0,0 +1,20 @@ +{ + "code": 200, + "msg": "success", + "data": [ + { + "id": 1, + "name": "آبیاری قطره‌ای", + "category": "موضعی", + "description": "آبیاری با دبی کم و راندمان بالا", + "water_efficiency_percent": 90.0, + "water_pressure_required": "۱-۲ اتمسفر", + "flow_rate": "۲-۸ لیتر در ساعت", + "coverage_area": "بسته به طراحی سیستم", + "soil_type": "اکثر خاک‌ها", + "climate_suitability": "گرم و خشک", + "created_at": "2025-03-20T10:00:00Z", + "updated_at": "2025-03-24T10:00:00Z" + } + ] +} diff --git a/external_api_adapter/json/ai/irrigation/methods/post_201.json b/external_api_adapter/json/ai/irrigation/methods/post_201.json new file mode 100644 index 0000000..2fba4f5 --- /dev/null +++ b/external_api_adapter/json/ai/irrigation/methods/post_201.json @@ -0,0 +1,18 @@ +{ + "code": 201, + "msg": "success", + "data": { + "id": 1, + "name": "آبیاری قطره‌ای", + "category": "موضعی", + "description": "آبیاری با دبی کم و راندمان بالا", + "water_efficiency_percent": 90.0, + "water_pressure_required": "۱-۲ اتمسفر", + "flow_rate": "۲-۸ لیتر در ساعت", + "coverage_area": "بسته به طراحی سیستم", + "soil_type": "اکثر خاک‌ها", + "climate_suitability": "گرم و خشک", + "created_at": "2025-03-20T10:00:00Z", + "updated_at": "2025-03-24T10:00:00Z" + } +} diff --git a/external_api_adapter/json/ai/irrigation/methods/post_400.json b/external_api_adapter/json/ai/irrigation/methods/post_400.json new file mode 100644 index 0000000..f72f28c --- /dev/null +++ b/external_api_adapter/json/ai/irrigation/methods/post_400.json @@ -0,0 +1,9 @@ +{ + "code": 400, + "msg": "داده نامعتبر.", + "data": { + "name": [ + "This field is required." + ] + } +} diff --git a/external_api_adapter/json/ai/irrigation/recommend/post_202.json b/external_api_adapter/json/ai/irrigation/recommend/post_202.json new file mode 100644 index 0000000..a5f230d --- /dev/null +++ b/external_api_adapter/json/ai/irrigation/recommend/post_202.json @@ -0,0 +1,8 @@ +{ + "code": 202, + "msg": "تسک توصیه آبیاری در صف قرار گرفت.", + "data": { + "task_id": "irr-task-123", + "status_url": "/api/irrigation/recommend/irr-task-123/status/" + } +} diff --git a/external_api_adapter/json/ai/irrigation/recommend/post_400.json b/external_api_adapter/json/ai/irrigation/recommend/post_400.json new file mode 100644 index 0000000..9fdc597 --- /dev/null +++ b/external_api_adapter/json/ai/irrigation/recommend/post_400.json @@ -0,0 +1,9 @@ +{ + "code": 400, + "msg": "داده نامعتبر.", + "data": { + "sensor_uuid": [ + "This field is required." + ] + } +} diff --git a/external_api_adapter/json/ai/irrigation/recommend/status/get_200_failure.json b/external_api_adapter/json/ai/irrigation/recommend/status/get_200_failure.json new file mode 100644 index 0000000..5f4e1f7 --- /dev/null +++ b/external_api_adapter/json/ai/irrigation/recommend/status/get_200_failure.json @@ -0,0 +1,9 @@ +{ + "code": 200, + "msg": "success", + "data": { + "task_id": "irr-task-123", + "status": "FAILURE", + "error": "خطا در دریافت توصیه آبیاری." + } +} diff --git a/external_api_adapter/json/ai/irrigation/recommend/status/get_200_pending.json b/external_api_adapter/json/ai/irrigation/recommend/status/get_200_pending.json new file mode 100644 index 0000000..498b382 --- /dev/null +++ b/external_api_adapter/json/ai/irrigation/recommend/status/get_200_pending.json @@ -0,0 +1,9 @@ +{ + "code": 200, + "msg": "success", + "data": { + "task_id": "irr-task-123", + "status": "PENDING", + "message": "تسک در صف یا یافت نشد." + } +} diff --git a/external_api_adapter/json/ai/irrigation/recommend/status/get_200_progress.json b/external_api_adapter/json/ai/irrigation/recommend/status/get_200_progress.json new file mode 100644 index 0000000..e8837dd --- /dev/null +++ b/external_api_adapter/json/ai/irrigation/recommend/status/get_200_progress.json @@ -0,0 +1,11 @@ +{ + "code": 200, + "msg": "success", + "data": { + "task_id": "irr-task-123", + "status": "PROGRESS", + "progress": { + "message": "در حال پردازش توصیه آبیاری..." + } + } +} diff --git a/external_api_adapter/json/ai/irrigation/recommend/status/get_200_success.json b/external_api_adapter/json/ai/irrigation/recommend/status/get_200_success.json new file mode 100644 index 0000000..46f9c90 --- /dev/null +++ b/external_api_adapter/json/ai/irrigation/recommend/status/get_200_success.json @@ -0,0 +1,37 @@ +{ + "code": 200, + "msg": "success", + "data": { + "task_id": "irr-task-123", + "status": "SUCCESS", + "result": { + "plan": { + "frequencyPerWeek": 3, + "durationMinutes": 42, + "bestTimeOfDay": "صبح زود", + "moistureLevel": 68, + "warning": "در صورت بارش موثر، نوبت سوم این هفته را حذف کنید." + }, + "raw_response": "{\"plan\":{\"frequencyPerWeek\":3,\"durationMinutes\":42}}", + "water_balance": { + "daily": [ + { + "forecast_date": "2025-03-25", + "et0_mm": 4.7, + "etc_mm": 5.6, + "effective_rainfall_mm": 0.0, + "gross_irrigation_mm": 6.2, + "irrigation_timing": "06:00-08:00" + } + ], + "crop_profile": { + "kc_initial": 0.6, + "kc_mid": 1.15, + "kc_end": 0.8 + }, + "active_kc": 1.15 + }, + "status": "completed" + } + } +} diff --git a/external_api_adapter/json/ai/plant/create-post_201.json b/external_api_adapter/json/ai/plant/create-post_201.json new file mode 100644 index 0000000..32c4614 --- /dev/null +++ b/external_api_adapter/json/ai/plant/create-post_201.json @@ -0,0 +1,18 @@ +{ + "code": 201, + "msg": "success", + "data": { + "id": 1, + "name": "گوجه‌فرنگی", + "light": "آفتاب کامل", + "watering": "منظم، هفته‌ای ۲ تا ۳ بار", + "soil": "لومی، غنی از مواد آلی", + "temperature": "۲۰ تا ۳۰ درجه سانتی‌گراد", + "planting_season": "بهار", + "harvest_time": "۷۰ تا ۹۰ روز پس از کاشت", + "spacing": "۴۵ تا ۶۰ سانتی‌متر", + "fertilizer": "کود NPK متعادل", + "created_at": "2025-03-20T10:00:00Z", + "updated_at": "2025-03-24T10:00:00Z" + } +} diff --git a/external_api_adapter/json/ai/plant/create-post_400.json b/external_api_adapter/json/ai/plant/create-post_400.json new file mode 100644 index 0000000..f72f28c --- /dev/null +++ b/external_api_adapter/json/ai/plant/create-post_400.json @@ -0,0 +1,9 @@ +{ + "code": 400, + "msg": "داده نامعتبر.", + "data": { + "name": [ + "This field is required." + ] + } +} diff --git a/external_api_adapter/json/ai/plant/detail-delete_200.json b/external_api_adapter/json/ai/plant/detail-delete_200.json new file mode 100644 index 0000000..b127160 --- /dev/null +++ b/external_api_adapter/json/ai/plant/detail-delete_200.json @@ -0,0 +1,5 @@ +{ + "code": 200, + "msg": "گیاه با موفقیت حذف شد.", + "data": null +} diff --git a/external_api_adapter/json/ai/plant/detail-delete_404.json b/external_api_adapter/json/ai/plant/detail-delete_404.json new file mode 100644 index 0000000..497519d --- /dev/null +++ b/external_api_adapter/json/ai/plant/detail-delete_404.json @@ -0,0 +1,5 @@ +{ + "code": 404, + "msg": "گیاه یافت نشد.", + "data": null +} diff --git a/external_api_adapter/json/ai/plant/detail-get_200.json b/external_api_adapter/json/ai/plant/detail-get_200.json new file mode 100644 index 0000000..af5f8ad --- /dev/null +++ b/external_api_adapter/json/ai/plant/detail-get_200.json @@ -0,0 +1,18 @@ +{ + "code": 200, + "msg": "success", + "data": { + "id": 1, + "name": "گوجه‌فرنگی", + "light": "آفتاب کامل", + "watering": "منظم، هفته‌ای ۲ تا ۳ بار", + "soil": "لومی، غنی از مواد آلی", + "temperature": "۲۰ تا ۳۰ درجه سانتی‌گراد", + "planting_season": "بهار", + "harvest_time": "۷۰ تا ۹۰ روز پس از کاشت", + "spacing": "۴۵ تا ۶۰ سانتی‌متر", + "fertilizer": "کود NPK متعادل", + "created_at": "2025-03-20T10:00:00Z", + "updated_at": "2025-03-24T10:00:00Z" + } +} diff --git a/external_api_adapter/json/ai/plant/detail-get_404.json b/external_api_adapter/json/ai/plant/detail-get_404.json new file mode 100644 index 0000000..497519d --- /dev/null +++ b/external_api_adapter/json/ai/plant/detail-get_404.json @@ -0,0 +1,5 @@ +{ + "code": 404, + "msg": "گیاه یافت نشد.", + "data": null +} diff --git a/external_api_adapter/json/ai/plant/detail-patch_200.json b/external_api_adapter/json/ai/plant/detail-patch_200.json new file mode 100644 index 0000000..af5f8ad --- /dev/null +++ b/external_api_adapter/json/ai/plant/detail-patch_200.json @@ -0,0 +1,18 @@ +{ + "code": 200, + "msg": "success", + "data": { + "id": 1, + "name": "گوجه‌فرنگی", + "light": "آفتاب کامل", + "watering": "منظم، هفته‌ای ۲ تا ۳ بار", + "soil": "لومی، غنی از مواد آلی", + "temperature": "۲۰ تا ۳۰ درجه سانتی‌گراد", + "planting_season": "بهار", + "harvest_time": "۷۰ تا ۹۰ روز پس از کاشت", + "spacing": "۴۵ تا ۶۰ سانتی‌متر", + "fertilizer": "کود NPK متعادل", + "created_at": "2025-03-20T10:00:00Z", + "updated_at": "2025-03-24T10:00:00Z" + } +} diff --git a/external_api_adapter/json/ai/plant/detail-patch_400.json b/external_api_adapter/json/ai/plant/detail-patch_400.json new file mode 100644 index 0000000..2e4dd22 --- /dev/null +++ b/external_api_adapter/json/ai/plant/detail-patch_400.json @@ -0,0 +1,9 @@ +{ + "code": 400, + "msg": "داده نامعتبر.", + "data": { + "name": [ + "This field may not be blank." + ] + } +} diff --git a/external_api_adapter/json/ai/plant/detail-patch_404.json b/external_api_adapter/json/ai/plant/detail-patch_404.json new file mode 100644 index 0000000..497519d --- /dev/null +++ b/external_api_adapter/json/ai/plant/detail-patch_404.json @@ -0,0 +1,5 @@ +{ + "code": 404, + "msg": "گیاه یافت نشد.", + "data": null +} diff --git a/external_api_adapter/json/ai/plant/detail-put_200.json b/external_api_adapter/json/ai/plant/detail-put_200.json new file mode 100644 index 0000000..af5f8ad --- /dev/null +++ b/external_api_adapter/json/ai/plant/detail-put_200.json @@ -0,0 +1,18 @@ +{ + "code": 200, + "msg": "success", + "data": { + "id": 1, + "name": "گوجه‌فرنگی", + "light": "آفتاب کامل", + "watering": "منظم، هفته‌ای ۲ تا ۳ بار", + "soil": "لومی، غنی از مواد آلی", + "temperature": "۲۰ تا ۳۰ درجه سانتی‌گراد", + "planting_season": "بهار", + "harvest_time": "۷۰ تا ۹۰ روز پس از کاشت", + "spacing": "۴۵ تا ۶۰ سانتی‌متر", + "fertilizer": "کود NPK متعادل", + "created_at": "2025-03-20T10:00:00Z", + "updated_at": "2025-03-24T10:00:00Z" + } +} diff --git a/external_api_adapter/json/ai/plant/detail-put_400.json b/external_api_adapter/json/ai/plant/detail-put_400.json new file mode 100644 index 0000000..2e4dd22 --- /dev/null +++ b/external_api_adapter/json/ai/plant/detail-put_400.json @@ -0,0 +1,9 @@ +{ + "code": 400, + "msg": "داده نامعتبر.", + "data": { + "name": [ + "This field may not be blank." + ] + } +} diff --git a/external_api_adapter/json/ai/plant/detail-put_404.json b/external_api_adapter/json/ai/plant/detail-put_404.json new file mode 100644 index 0000000..497519d --- /dev/null +++ b/external_api_adapter/json/ai/plant/detail-put_404.json @@ -0,0 +1,5 @@ +{ + "code": 404, + "msg": "گیاه یافت نشد.", + "data": null +} diff --git a/external_api_adapter/json/ai/plant/fetch-info-post_200.json b/external_api_adapter/json/ai/plant/fetch-info-post_200.json new file mode 100644 index 0000000..af5f8ad --- /dev/null +++ b/external_api_adapter/json/ai/plant/fetch-info-post_200.json @@ -0,0 +1,18 @@ +{ + "code": 200, + "msg": "success", + "data": { + "id": 1, + "name": "گوجه‌فرنگی", + "light": "آفتاب کامل", + "watering": "منظم، هفته‌ای ۲ تا ۳ بار", + "soil": "لومی، غنی از مواد آلی", + "temperature": "۲۰ تا ۳۰ درجه سانتی‌گراد", + "planting_season": "بهار", + "harvest_time": "۷۰ تا ۹۰ روز پس از کاشت", + "spacing": "۴۵ تا ۶۰ سانتی‌متر", + "fertilizer": "کود NPK متعادل", + "created_at": "2025-03-20T10:00:00Z", + "updated_at": "2025-03-24T10:00:00Z" + } +} diff --git a/external_api_adapter/json/ai/plant/fetch-info-post_400.json b/external_api_adapter/json/ai/plant/fetch-info-post_400.json new file mode 100644 index 0000000..e4bbdd2 --- /dev/null +++ b/external_api_adapter/json/ai/plant/fetch-info-post_400.json @@ -0,0 +1,5 @@ +{ + "code": 400, + "msg": "نام گیاه الزامی است.", + "data": null +} diff --git a/external_api_adapter/json/ai/plant/fetch-info-post_503.json b/external_api_adapter/json/ai/plant/fetch-info-post_503.json new file mode 100644 index 0000000..f4911a7 --- /dev/null +++ b/external_api_adapter/json/ai/plant/fetch-info-post_503.json @@ -0,0 +1,5 @@ +{ + "code": 503, + "msg": "سرویس API هنوز پیاده‌سازی نشده است.", + "data": null +} diff --git a/external_api_adapter/json/ai/plant/list-get_200.json b/external_api_adapter/json/ai/plant/list-get_200.json new file mode 100644 index 0000000..e6586a2 --- /dev/null +++ b/external_api_adapter/json/ai/plant/list-get_200.json @@ -0,0 +1,20 @@ +{ + "code": 200, + "msg": "success", + "data": [ + { + "id": 1, + "name": "گوجه‌فرنگی", + "light": "آفتاب کامل", + "watering": "منظم، هفته‌ای ۲ تا ۳ بار", + "soil": "لومی، غنی از مواد آلی", + "temperature": "۲۰ تا ۳۰ درجه سانتی‌گراد", + "planting_season": "بهار", + "harvest_time": "۷۰ تا ۹۰ روز پس از کاشت", + "spacing": "۴۵ تا ۶۰ سانتی‌متر", + "fertilizer": "کود NPK متعادل", + "created_at": "2025-03-20T10:00:00Z", + "updated_at": "2025-03-24T10:00:00Z" + } + ] +} diff --git a/external_api_adapter/json/ai/predict/get_200_success.json b/external_api_adapter/json/ai/predict/get_200_success.json new file mode 100644 index 0000000..0fe8cc1 --- /dev/null +++ b/external_api_adapter/json/ai/predict/get_200_success.json @@ -0,0 +1,9 @@ +{ + "status": "success", + "service": "ai", + "path": "/predict", + "result": { + "prediction": "healthy", + "confidence": 0.97 + } +} diff --git a/external_api_adapter/json/ai/rag/chat-post_200_stream.json b/external_api_adapter/json/ai/rag/chat-post_200_stream.json new file mode 100644 index 0000000..ead2147 --- /dev/null +++ b/external_api_adapter/json/ai/rag/chat-post_200_stream.json @@ -0,0 +1,4 @@ +{ + "content_type": "text/plain; charset=utf-8", + "body": "سلام، برای بازیابی رطوبت خاک بهتر است آبیاری صبح‌گاهی را تنظیم کنید." +} diff --git a/external_api_adapter/json/ai/rag/chat-post_400_invalid_service.json b/external_api_adapter/json/ai/rag/chat-post_400_invalid_service.json new file mode 100644 index 0000000..c484816 --- /dev/null +++ b/external_api_adapter/json/ai/rag/chat-post_400_invalid_service.json @@ -0,0 +1,4 @@ +{ + "code": 400, + "msg": "service_id نامعتبر است: unknown_service" +} diff --git a/external_api_adapter/json/ai/rag/chat-post_400_missing_query.json b/external_api_adapter/json/ai/rag/chat-post_400_missing_query.json new file mode 100644 index 0000000..b491272 --- /dev/null +++ b/external_api_adapter/json/ai/rag/chat-post_400_missing_query.json @@ -0,0 +1,4 @@ +{ + "code": 400, + "msg": "پارامتر query الزامی است." +} diff --git a/external_api_adapter/json/ai/rag/chat-post_400_missing_user.json b/external_api_adapter/json/ai/rag/chat-post_400_missing_user.json new file mode 100644 index 0000000..3615785 --- /dev/null +++ b/external_api_adapter/json/ai/rag/chat-post_400_missing_user.json @@ -0,0 +1,4 @@ +{ + "code": 400, + "msg": "برای این service_id، پارامتر user_id الزامی است." +} diff --git a/external_api_adapter/json/ai/rag/fertilization/post_202.json b/external_api_adapter/json/ai/rag/fertilization/post_202.json new file mode 100644 index 0000000..8fe7cc7 --- /dev/null +++ b/external_api_adapter/json/ai/rag/fertilization/post_202.json @@ -0,0 +1,8 @@ +{ + "code": 202, + "msg": "تسک توصیه کودهی در صف قرار گرفت.", + "data": { + "task_id": "rag-fert-123", + "status_url": "/api/rag/recommend/fertilization/rag-fert-123/status/" + } +} diff --git a/external_api_adapter/json/ai/rag/fertilization/post_400.json b/external_api_adapter/json/ai/rag/fertilization/post_400.json new file mode 100644 index 0000000..6bea070 --- /dev/null +++ b/external_api_adapter/json/ai/rag/fertilization/post_400.json @@ -0,0 +1,5 @@ +{ + "code": 400, + "msg": "پارامتر sensor_uuid الزامی است.", + "data": null +} diff --git a/external_api_adapter/json/ai/rag/fertilization/status/get_200_failure.json b/external_api_adapter/json/ai/rag/fertilization/status/get_200_failure.json new file mode 100644 index 0000000..971dd04 --- /dev/null +++ b/external_api_adapter/json/ai/rag/fertilization/status/get_200_failure.json @@ -0,0 +1,9 @@ +{ + "code": 200, + "msg": "success", + "data": { + "task_id": "rag-fert-123", + "status": "FAILURE", + "error": "خطا در دریافت توصیه کودهی." + } +} diff --git a/external_api_adapter/json/ai/rag/fertilization/status/get_200_pending.json b/external_api_adapter/json/ai/rag/fertilization/status/get_200_pending.json new file mode 100644 index 0000000..9c9eca6 --- /dev/null +++ b/external_api_adapter/json/ai/rag/fertilization/status/get_200_pending.json @@ -0,0 +1,9 @@ +{ + "code": 200, + "msg": "success", + "data": { + "task_id": "rag-fert-123", + "status": "PENDING", + "message": "تسک در صف یا یافت نشد." + } +} diff --git a/external_api_adapter/json/ai/rag/fertilization/status/get_200_progress.json b/external_api_adapter/json/ai/rag/fertilization/status/get_200_progress.json new file mode 100644 index 0000000..4cd6d57 --- /dev/null +++ b/external_api_adapter/json/ai/rag/fertilization/status/get_200_progress.json @@ -0,0 +1,11 @@ +{ + "code": 200, + "msg": "success", + "data": { + "task_id": "rag-fert-123", + "status": "PROGRESS", + "progress": { + "message": "در حال پردازش توصیه کودهی..." + } + } +} diff --git a/external_api_adapter/json/ai/rag/fertilization/status/get_200_success.json b/external_api_adapter/json/ai/rag/fertilization/status/get_200_success.json new file mode 100644 index 0000000..cea132a --- /dev/null +++ b/external_api_adapter/json/ai/rag/fertilization/status/get_200_success.json @@ -0,0 +1,19 @@ +{ + "code": 200, + "msg": "success", + "data": { + "task_id": "rag-fert-123", + "status": "SUCCESS", + "result": { + "plan": { + "npkRatio": "20-20-20", + "amountPerHectare": "150 kg/ha", + "applicationMethod": "کودآبیاری در دو نوبت", + "applicationInterval": "هر ۱۰ روز", + "reasoning": "نیتروژن و پتاسیم خاک در محدوده متوسط است و گیاه در فاز رویشی نیاز تغذیه‌ای بالاتری دارد." + }, + "raw_response": "{\"plan\":{\"npkRatio\":\"20-20-20\"}}", + "status": "completed" + } + } +} diff --git a/external_api_adapter/json/ai/rag/irrigation/post_202.json b/external_api_adapter/json/ai/rag/irrigation/post_202.json new file mode 100644 index 0000000..bcc6db9 --- /dev/null +++ b/external_api_adapter/json/ai/rag/irrigation/post_202.json @@ -0,0 +1,8 @@ +{ + "code": 202, + "msg": "تسک توصیه آبیاری در صف قرار گرفت.", + "data": { + "task_id": "rag-irr-123", + "status_url": "/api/rag/recommend/irrigation/rag-irr-123/status/" + } +} diff --git a/external_api_adapter/json/ai/rag/irrigation/post_400.json b/external_api_adapter/json/ai/rag/irrigation/post_400.json new file mode 100644 index 0000000..6bea070 --- /dev/null +++ b/external_api_adapter/json/ai/rag/irrigation/post_400.json @@ -0,0 +1,5 @@ +{ + "code": 400, + "msg": "پارامتر sensor_uuid الزامی است.", + "data": null +} diff --git a/external_api_adapter/json/ai/rag/irrigation/status/get_200_failure.json b/external_api_adapter/json/ai/rag/irrigation/status/get_200_failure.json new file mode 100644 index 0000000..bffa098 --- /dev/null +++ b/external_api_adapter/json/ai/rag/irrigation/status/get_200_failure.json @@ -0,0 +1,9 @@ +{ + "code": 200, + "msg": "success", + "data": { + "task_id": "rag-irr-123", + "status": "FAILURE", + "error": "خطا در دریافت توصیه آبیاری." + } +} diff --git a/external_api_adapter/json/ai/rag/irrigation/status/get_200_pending.json b/external_api_adapter/json/ai/rag/irrigation/status/get_200_pending.json new file mode 100644 index 0000000..1074e86 --- /dev/null +++ b/external_api_adapter/json/ai/rag/irrigation/status/get_200_pending.json @@ -0,0 +1,9 @@ +{ + "code": 200, + "msg": "success", + "data": { + "task_id": "rag-irr-123", + "status": "PENDING", + "message": "تسک در صف یا یافت نشد." + } +} diff --git a/external_api_adapter/json/ai/rag/irrigation/status/get_200_progress.json b/external_api_adapter/json/ai/rag/irrigation/status/get_200_progress.json new file mode 100644 index 0000000..3b88990 --- /dev/null +++ b/external_api_adapter/json/ai/rag/irrigation/status/get_200_progress.json @@ -0,0 +1,11 @@ +{ + "code": 200, + "msg": "success", + "data": { + "task_id": "rag-irr-123", + "status": "PROGRESS", + "progress": { + "message": "در حال پردازش توصیه آبیاری..." + } + } +} diff --git a/external_api_adapter/json/ai/rag/irrigation/status/get_200_success.json b/external_api_adapter/json/ai/rag/irrigation/status/get_200_success.json new file mode 100644 index 0000000..5679648 --- /dev/null +++ b/external_api_adapter/json/ai/rag/irrigation/status/get_200_success.json @@ -0,0 +1,37 @@ +{ + "code": 200, + "msg": "success", + "data": { + "task_id": "rag-irr-123", + "status": "SUCCESS", + "result": { + "plan": { + "frequencyPerWeek": 3, + "durationMinutes": 42, + "bestTimeOfDay": "صبح زود", + "moistureLevel": 68, + "warning": "در صورت بارش موثر، نوبت سوم این هفته را حذف کنید." + }, + "raw_response": "{\"plan\":{\"frequencyPerWeek\":3,\"durationMinutes\":42}}", + "water_balance": { + "daily": [ + { + "forecast_date": "2025-03-25", + "et0_mm": 4.7, + "etc_mm": 5.6, + "effective_rainfall_mm": 0.0, + "gross_irrigation_mm": 6.2, + "irrigation_timing": "06:00-08:00" + } + ], + "crop_profile": { + "kc_initial": 0.6, + "kc_mid": 1.15, + "kc_end": 0.8 + }, + "active_kc": 1.15 + }, + "status": "completed" + } + } +} diff --git a/external_api_adapter/json/ai/sensor-data/parameters-post_201.json b/external_api_adapter/json/ai/sensor-data/parameters-post_201.json new file mode 100644 index 0000000..9de5e9d --- /dev/null +++ b/external_api_adapter/json/ai/sensor-data/parameters-post_201.json @@ -0,0 +1,12 @@ +{ + "code": 201, + "msg": "success", + "data": { + "id": 3, + "code": "soil_moisture", + "name_fa": "رطوبت خاک", + "unit": "%", + "created_at": "2025-03-24T10:00:00Z", + "action": "added" + } +} diff --git a/external_api_adapter/json/ai/sensor-data/parameters-post_400.json b/external_api_adapter/json/ai/sensor-data/parameters-post_400.json new file mode 100644 index 0000000..98b89aa --- /dev/null +++ b/external_api_adapter/json/ai/sensor-data/parameters-post_400.json @@ -0,0 +1,9 @@ +{ + "code": 400, + "msg": "داده نامعتبر.", + "data": { + "code": [ + "This field is required." + ] + } +} diff --git a/external_api_adapter/json/ai/sensor-data/update-patch_200.json b/external_api_adapter/json/ai/sensor-data/update-patch_200.json new file mode 100644 index 0000000..5bc26e3 --- /dev/null +++ b/external_api_adapter/json/ai/sensor-data/update-patch_200.json @@ -0,0 +1,20 @@ +{ + "code": 200, + "msg": "success", + "data": { + "uuid_sensor": "550e8400-e29b-41d4-a716-446655440000", + "location_id": 12, + "soil_moisture": 45.2, + "soil_temperature": 22.5, + "soil_ph": 6.8, + "electrical_conductivity": 1.2, + "nitrogen": 30.0, + "phosphorus": 15.0, + "potassium": 20.0, + "plant_ids": [ + 1 + ], + "created_at": "2025-03-20T10:00:00Z", + "updated_at": "2025-03-24T10:00:00Z" + } +} diff --git a/external_api_adapter/json/ai/sensor-data/update-patch_400.json b/external_api_adapter/json/ai/sensor-data/update-patch_400.json new file mode 100644 index 0000000..cf343c5 --- /dev/null +++ b/external_api_adapter/json/ai/sensor-data/update-patch_400.json @@ -0,0 +1,9 @@ +{ + "code": 400, + "msg": "داده نامعتبر.", + "data": { + "location_id": [ + "This field is required." + ] + } +} diff --git a/external_api_adapter/json/ai/sensor-data/update-patch_404.json b/external_api_adapter/json/ai/sensor-data/update-patch_404.json new file mode 100644 index 0000000..107ea33 --- /dev/null +++ b/external_api_adapter/json/ai/sensor-data/update-patch_404.json @@ -0,0 +1,5 @@ +{ + "code": 404, + "msg": "location_id یافت نشد.", + "data": null +} diff --git a/external_api_adapter/json/ai/sensor-data/update-put_200.json b/external_api_adapter/json/ai/sensor-data/update-put_200.json new file mode 100644 index 0000000..5bc26e3 --- /dev/null +++ b/external_api_adapter/json/ai/sensor-data/update-put_200.json @@ -0,0 +1,20 @@ +{ + "code": 200, + "msg": "success", + "data": { + "uuid_sensor": "550e8400-e29b-41d4-a716-446655440000", + "location_id": 12, + "soil_moisture": 45.2, + "soil_temperature": 22.5, + "soil_ph": 6.8, + "electrical_conductivity": 1.2, + "nitrogen": 30.0, + "phosphorus": 15.0, + "potassium": 20.0, + "plant_ids": [ + 1 + ], + "created_at": "2025-03-20T10:00:00Z", + "updated_at": "2025-03-24T10:00:00Z" + } +} diff --git a/external_api_adapter/json/ai/sensor-data/update-put_400.json b/external_api_adapter/json/ai/sensor-data/update-put_400.json new file mode 100644 index 0000000..cf343c5 --- /dev/null +++ b/external_api_adapter/json/ai/sensor-data/update-put_400.json @@ -0,0 +1,9 @@ +{ + "code": 400, + "msg": "داده نامعتبر.", + "data": { + "location_id": [ + "This field is required." + ] + } +} diff --git a/external_api_adapter/json/ai/sensor-data/update-put_404.json b/external_api_adapter/json/ai/sensor-data/update-put_404.json new file mode 100644 index 0000000..107ea33 --- /dev/null +++ b/external_api_adapter/json/ai/sensor-data/update-put_404.json @@ -0,0 +1,5 @@ +{ + "code": 404, + "msg": "location_id یافت نشد.", + "data": null +} diff --git a/external_api_adapter/json/ai/soil-data/get_200_database.json b/external_api_adapter/json/ai/soil-data/get_200_database.json new file mode 100644 index 0000000..87becb1 --- /dev/null +++ b/external_api_adapter/json/ai/soil-data/get_200_database.json @@ -0,0 +1,63 @@ +{ + "code": 200, + "msg": "success", + "data": { + "source": "database", + "id": 12, + "lon": "51.389000", + "lat": "35.689200", + "depths": [ + { + "depth_label": "0-5cm", + "bdod": 1.31, + "cec": 18.4, + "cfvo": 2.0, + "clay": 24.0, + "nitrogen": 0.18, + "ocd": 32.0, + "ocs": 4.1, + "phh2o": 7.2, + "sand": 34.0, + "silt": 42.0, + "soc": 1.6, + "wv0010": 0.31, + "wv0033": 0.22, + "wv1500": 0.11 + }, + { + "depth_label": "5-15cm", + "bdod": 1.35, + "cec": 17.2, + "cfvo": 2.3, + "clay": 26.0, + "nitrogen": 0.16, + "ocd": 28.0, + "ocs": 3.7, + "phh2o": 7.1, + "sand": 36.0, + "silt": 38.0, + "soc": 1.4, + "wv0010": 0.29, + "wv0033": 0.2, + "wv1500": 0.1 + }, + { + "depth_label": "15-30cm", + "bdod": 1.39, + "cec": 15.8, + "cfvo": 2.8, + "clay": 28.0, + "nitrogen": 0.13, + "ocd": 22.0, + "ocs": 3.2, + "phh2o": 7.0, + "sand": 38.0, + "silt": 34.0, + "soc": 1.1, + "wv0010": 0.26, + "wv0033": 0.18, + "wv1500": 0.09 + } + ] + } +} diff --git a/external_api_adapter/json/ai/soil-data/get_202_queued.json b/external_api_adapter/json/ai/soil-data/get_202_queued.json new file mode 100644 index 0000000..5a06843 --- /dev/null +++ b/external_api_adapter/json/ai/soil-data/get_202_queued.json @@ -0,0 +1,11 @@ +{ + "code": 202, + "msg": "تسک در صف. وضعیت را با task_id بررسی کنید.", + "data": { + "source": "task", + "task_id": "soil-task-123", + "lon": 51.389, + "lat": 35.6892, + "status_url": "/api/soil-data/tasks/soil-task-123/status/" + } +} diff --git a/external_api_adapter/json/ai/soil-data/get_400.json b/external_api_adapter/json/ai/soil-data/get_400.json new file mode 100644 index 0000000..bfc19ab --- /dev/null +++ b/external_api_adapter/json/ai/soil-data/get_400.json @@ -0,0 +1,12 @@ +{ + "code": 400, + "msg": "داده نامعتبر.", + "data": { + "lat": [ + "This field is required." + ], + "lon": [ + "This field is required." + ] + } +} diff --git a/external_api_adapter/json/ai/soil-data/post_200_database.json b/external_api_adapter/json/ai/soil-data/post_200_database.json new file mode 100644 index 0000000..87becb1 --- /dev/null +++ b/external_api_adapter/json/ai/soil-data/post_200_database.json @@ -0,0 +1,63 @@ +{ + "code": 200, + "msg": "success", + "data": { + "source": "database", + "id": 12, + "lon": "51.389000", + "lat": "35.689200", + "depths": [ + { + "depth_label": "0-5cm", + "bdod": 1.31, + "cec": 18.4, + "cfvo": 2.0, + "clay": 24.0, + "nitrogen": 0.18, + "ocd": 32.0, + "ocs": 4.1, + "phh2o": 7.2, + "sand": 34.0, + "silt": 42.0, + "soc": 1.6, + "wv0010": 0.31, + "wv0033": 0.22, + "wv1500": 0.11 + }, + { + "depth_label": "5-15cm", + "bdod": 1.35, + "cec": 17.2, + "cfvo": 2.3, + "clay": 26.0, + "nitrogen": 0.16, + "ocd": 28.0, + "ocs": 3.7, + "phh2o": 7.1, + "sand": 36.0, + "silt": 38.0, + "soc": 1.4, + "wv0010": 0.29, + "wv0033": 0.2, + "wv1500": 0.1 + }, + { + "depth_label": "15-30cm", + "bdod": 1.39, + "cec": 15.8, + "cfvo": 2.8, + "clay": 28.0, + "nitrogen": 0.13, + "ocd": 22.0, + "ocs": 3.2, + "phh2o": 7.0, + "sand": 38.0, + "silt": 34.0, + "soc": 1.1, + "wv0010": 0.26, + "wv0033": 0.18, + "wv1500": 0.09 + } + ] + } +} diff --git a/external_api_adapter/json/ai/soil-data/post_202_queued.json b/external_api_adapter/json/ai/soil-data/post_202_queued.json new file mode 100644 index 0000000..5a06843 --- /dev/null +++ b/external_api_adapter/json/ai/soil-data/post_202_queued.json @@ -0,0 +1,11 @@ +{ + "code": 202, + "msg": "تسک در صف. وضعیت را با task_id بررسی کنید.", + "data": { + "source": "task", + "task_id": "soil-task-123", + "lon": 51.389, + "lat": 35.6892, + "status_url": "/api/soil-data/tasks/soil-task-123/status/" + } +} diff --git a/external_api_adapter/json/ai/soil-data/post_400.json b/external_api_adapter/json/ai/soil-data/post_400.json new file mode 100644 index 0000000..4d32daf --- /dev/null +++ b/external_api_adapter/json/ai/soil-data/post_400.json @@ -0,0 +1,9 @@ +{ + "code": 400, + "msg": "داده نامعتبر.", + "data": { + "lat": [ + "A valid number is required." + ] + } +} diff --git a/external_api_adapter/json/ai/soil-data/status/get_200_failure.json b/external_api_adapter/json/ai/soil-data/status/get_200_failure.json new file mode 100644 index 0000000..a0c0697 --- /dev/null +++ b/external_api_adapter/json/ai/soil-data/status/get_200_failure.json @@ -0,0 +1,9 @@ +{ + "code": 200, + "msg": "success", + "data": { + "task_id": "soil-task-123", + "status": "FAILURE", + "error": "خطا در واکشی داده خاک." + } +} diff --git a/external_api_adapter/json/ai/soil-data/status/get_200_pending.json b/external_api_adapter/json/ai/soil-data/status/get_200_pending.json new file mode 100644 index 0000000..b4965ce --- /dev/null +++ b/external_api_adapter/json/ai/soil-data/status/get_200_pending.json @@ -0,0 +1,9 @@ +{ + "code": 200, + "msg": "success", + "data": { + "task_id": "soil-task-123", + "status": "PENDING", + "message": "تسک در صف یا یافت نشد." + } +} diff --git a/external_api_adapter/json/ai/soil-data/status/get_200_progress.json b/external_api_adapter/json/ai/soil-data/status/get_200_progress.json new file mode 100644 index 0000000..29ecfe5 --- /dev/null +++ b/external_api_adapter/json/ai/soil-data/status/get_200_progress.json @@ -0,0 +1,12 @@ +{ + "code": 200, + "msg": "success", + "data": { + "task_id": "soil-task-123", + "status": "PROGRESS", + "progress": { + "step": "fetch", + "percent": 60 + } + } +} diff --git a/external_api_adapter/json/ai/soil-data/status/get_200_success.json b/external_api_adapter/json/ai/soil-data/status/get_200_success.json new file mode 100644 index 0000000..04d0827 --- /dev/null +++ b/external_api_adapter/json/ai/soil-data/status/get_200_success.json @@ -0,0 +1,67 @@ +{ + "code": 200, + "msg": "success", + "data": { + "task_id": "soil-task-123", + "status": "SUCCESS", + "result": { + "source": "database", + "id": 12, + "lon": "51.389000", + "lat": "35.689200", + "depths": [ + { + "depth_label": "0-5cm", + "bdod": 1.31, + "cec": 18.4, + "cfvo": 2.0, + "clay": 24.0, + "nitrogen": 0.18, + "ocd": 32.0, + "ocs": 4.1, + "phh2o": 7.2, + "sand": 34.0, + "silt": 42.0, + "soc": 1.6, + "wv0010": 0.31, + "wv0033": 0.22, + "wv1500": 0.11 + }, + { + "depth_label": "5-15cm", + "bdod": 1.35, + "cec": 17.2, + "cfvo": 2.3, + "clay": 26.0, + "nitrogen": 0.16, + "ocd": 28.0, + "ocs": 3.7, + "phh2o": 7.1, + "sand": 36.0, + "silt": 38.0, + "soc": 1.4, + "wv0010": 0.29, + "wv0033": 0.2, + "wv1500": 0.1 + }, + { + "depth_label": "15-30cm", + "bdod": 1.39, + "cec": 15.8, + "cfvo": 2.8, + "clay": 28.0, + "nitrogen": 0.13, + "ocd": 22.0, + "ocs": 3.2, + "phh2o": 7.0, + "sand": 38.0, + "silt": 34.0, + "soc": 1.1, + "wv0010": 0.26, + "wv0033": 0.18, + "wv1500": 0.09 + } + ] + } + } +} diff --git a/external_api_adapter/json/ai/tasks/post_200.json b/external_api_adapter/json/ai/tasks/post_200.json new file mode 100644 index 0000000..8fc9fe2 --- /dev/null +++ b/external_api_adapter/json/ai/tasks/post_200.json @@ -0,0 +1,7 @@ +{ + "code": 200, + "msg": "success", + "data": { + "task_id": "sample-task-123" + } +} diff --git a/external_api_adapter/json/ai/tasks/status/get_200_failure.json b/external_api_adapter/json/ai/tasks/status/get_200_failure.json new file mode 100644 index 0000000..2c34884 --- /dev/null +++ b/external_api_adapter/json/ai/tasks/status/get_200_failure.json @@ -0,0 +1,9 @@ +{ + "code": 200, + "msg": "success", + "data": { + "task_id": "sample-task-123", + "status": "FAILURE", + "error": "Sample task failed." + } +} diff --git a/external_api_adapter/json/ai/tasks/status/get_200_pending.json b/external_api_adapter/json/ai/tasks/status/get_200_pending.json new file mode 100644 index 0000000..c6ce7cb --- /dev/null +++ b/external_api_adapter/json/ai/tasks/status/get_200_pending.json @@ -0,0 +1,9 @@ +{ + "code": 200, + "msg": "success", + "data": { + "task_id": "sample-task-123", + "status": "PENDING", + "message": "تسک در صف یا یافت نشد." + } +} diff --git a/external_api_adapter/json/ai/tasks/status/get_200_progress.json b/external_api_adapter/json/ai/tasks/status/get_200_progress.json new file mode 100644 index 0000000..abb1b76 --- /dev/null +++ b/external_api_adapter/json/ai/tasks/status/get_200_progress.json @@ -0,0 +1,13 @@ +{ + "code": 200, + "msg": "success", + "data": { + "task_id": "sample-task-123", + "status": "PROGRESS", + "progress": { + "current": 1, + "total": 3, + "message": "در حال پردازش..." + } + } +} diff --git a/external_api_adapter/json/ai/tasks/status/get_200_success.json b/external_api_adapter/json/ai/tasks/status/get_200_success.json new file mode 100644 index 0000000..5b55965 --- /dev/null +++ b/external_api_adapter/json/ai/tasks/status/get_200_success.json @@ -0,0 +1,9 @@ +{ + "code": 200, + "msg": "success", + "data": { + "task_id": "sample-task-123", + "status": "SUCCESS", + "result": "done" + } +} diff --git a/external_api_adapter/json/sensor_hub/.gitkeep b/external_api_adapter/json/sensor_hub/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/external_api_adapter/json/sensor_hub/.gitkeep @@ -0,0 +1 @@ + diff --git a/external_api_adapter/mock_loader.py b/external_api_adapter/mock_loader.py new file mode 100644 index 0000000..0a2d13b --- /dev/null +++ b/external_api_adapter/mock_loader.py @@ -0,0 +1,89 @@ +import json +from dataclasses import dataclass +from pathlib import Path + +from .exceptions import MockDirectoryNotFound, MockFileNotFound + + +@dataclass +class LoadedMockResponse: + data: object + status_code: int + file_path: str + + +class MockLoader: + def __init__(self, base_path=None): + self.base_path = Path(base_path or Path(__file__).resolve().parent / "json") + + def load(self, service_name, path, method): + mock_files = self._find_mock_files(service_name=service_name, path=path, method=method) + if not mock_files: + raise MockFileNotFound( + f"No mock file found for service='{service_name}' path='{path}' method='{method}'." + ) + + selected_file = sorted(mock_files, key=self._mock_file_priority)[0] + with selected_file.open("r", encoding="utf-8") as file: + return LoadedMockResponse( + data=json.load(file), + status_code=self._extract_status_code(selected_file), + file_path=str(selected_file), + ) + + def _find_mock_files(self, service_name, path, method): + service_root = self.base_path / service_name + directory_path = service_root / self._build_directory_path(path) + pattern = f"{method.lower()}_*.json" + + if directory_path.exists() and directory_path.is_dir(): + return list(directory_path.glob(pattern)) + + leaf_name = self._extract_leaf_name(path) + parent_directory = directory_path.parent + if parent_directory.exists() and parent_directory.is_dir(): + flat_pattern = f"{leaf_name}-{method.lower()}_*.json" + flat_files = list(parent_directory.glob(flat_pattern)) + if flat_files: + return flat_files + + raise MockDirectoryNotFound( + f"Mock directory not found for service='{service_name}' path='{path}': {directory_path}" + ) + + @staticmethod + def _build_directory_path(path): + normalized = str(path).strip().strip("/") + if not normalized: + return Path(".") + return Path(*normalized.split("/")) + + @staticmethod + def _extract_status_code(file_path): + parts = file_path.stem.split("_") + if len(parts) < 2: + return 200 + try: + return int(parts[1]) + except ValueError: + return 200 + + @staticmethod + def _extract_leaf_name(path): + normalized = str(path).strip().strip("/") + if not normalized: + return "" + return normalized.split("/")[-1] + + @staticmethod + def _mock_file_priority(file_path): + stem = file_path.stem.lower() + status_code = MockLoader._extract_status_code(file_path) + keyword_rank = 2 + if "success" in stem: + keyword_rank = 0 + elif "stream" in stem: + keyword_rank = 0 + elif "pending" in stem or "progress" in stem: + keyword_rank = 1 + return (status_code >= 400, keyword_rank, stem) diff --git a/external_api_adapter/services.py b/external_api_adapter/services.py new file mode 100644 index 0000000..835fb6e --- /dev/null +++ b/external_api_adapter/services.py @@ -0,0 +1,14 @@ +from django.conf import settings + +from .exceptions import ServiceNotFound + + +class ServiceRegistry: + def __init__(self): + self._services = getattr(settings, "EXTERNAL_SERVICES", {}) + + def get(self, service_name): + service = self._services.get(service_name) + if service is None: + raise ServiceNotFound(f"Unknown external service: '{service_name}'") + return service diff --git a/farm_ai_assistant/views.py b/farm_ai_assistant/views.py index 7272547..f32a35e 100644 --- a/farm_ai_assistant/views.py +++ b/farm_ai_assistant/views.py @@ -5,6 +5,7 @@ Response format: {"status": "success", "data": }. HTTP 200 only. No processing, validation, or use of input parameters in responses. """ +from django.http import HttpResponse from rest_framework import serializers, status from rest_framework.response import Response from rest_framework.views import APIView @@ -12,7 +13,8 @@ from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import extend_schema from config.swagger import status_response -from .mock_data import CHAT_RESPONSE_DATA, CONTEXT_RESPONSE_DATA +from external_api_adapter import request as external_api_request +from .mock_data import CONTEXT_RESPONSE_DATA class ContextView(APIView): @@ -80,7 +82,16 @@ class ChatView(APIView): responses={200: status_response("FarmAiAssistantChatResponse", data=serializers.JSONField())}, ) def post(self, request): - return Response( - {"status": "success", "data": CHAT_RESPONSE_DATA}, - status=status.HTTP_200_OK, + adapter_response = external_api_request( + "ai", + "/rag/chat", + method="POST", + payload=request.data, ) + if isinstance(adapter_response.data, dict) and "body" in adapter_response.data: + return HttpResponse( + adapter_response.data["body"], + status=adapter_response.status_code, + content_type=adapter_response.data.get("content_type", "text/plain; charset=utf-8"), + ) + return Response(adapter_response.data, status=adapter_response.status_code) diff --git a/fertilization_recommendation/views.py b/fertilization_recommendation/views.py index 520d7a2..c68100e 100644 --- a/fertilization_recommendation/views.py +++ b/fertilization_recommendation/views.py @@ -12,7 +12,8 @@ from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import extend_schema from config.swagger import status_response -from .mock_data import CONFIG_RESPONSE_DATA, RECOMMEND_RESPONSE_DATA +from external_api_adapter import request as external_api_request +from .mock_data import CONFIG_RESPONSE_DATA class ConfigView(APIView): @@ -74,7 +75,10 @@ class RecommendView(APIView): responses={200: status_response("FertilizationRecommendResponse", data=serializers.JSONField())}, ) def post(self, request): - return Response( - {"status": "success", "data": RECOMMEND_RESPONSE_DATA}, - status=status.HTTP_200_OK, + adapter_response = external_api_request( + "ai", + "/fertilization/recommend", + method="POST", + payload=request.data, ) + return Response(adapter_response.data, status=adapter_response.status_code) diff --git a/irrigation_recommendation/views.py b/irrigation_recommendation/views.py index f5b2d2a..8431b77 100644 --- a/irrigation_recommendation/views.py +++ b/irrigation_recommendation/views.py @@ -12,7 +12,8 @@ from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import extend_schema from config.swagger import status_response -from .mock_data import CONFIG_RESPONSE_DATA, RECOMMEND_RESPONSE_DATA +from external_api_adapter import request as external_api_request +from .mock_data import CONFIG_RESPONSE_DATA class ConfigView(APIView): @@ -74,7 +75,10 @@ class RecommendView(APIView): responses={200: status_response("IrrigationRecommendResponse", data=serializers.JSONField())}, ) def post(self, request): - return Response( - {"status": "success", "data": RECOMMEND_RESPONSE_DATA}, - status=status.HTTP_200_OK, + adapter_response = external_api_request( + "ai", + "/irrigation/recommend", + method="POST", + payload=request.data, ) + return Response(adapter_response.data, status=adapter_response.status_code) diff --git a/requirements.txt b/requirements.txt index 1268019..73eeaf3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,5 +10,5 @@ redis>=5.0,<5.1 mysqlclient>=2.2,<2.3 gunicorn>=22,<23 python-dotenv>=1.0,<1.1 - +requests>=2.31,<2.33