diff --git a/access_control/views.py b/access_control/views.py index e3e1bb9..bb812aa 100644 --- a/access_control/views.py +++ b/access_control/views.py @@ -2,7 +2,7 @@ from rest_framework import status from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView -from drf_spectacular.utils import extend_schema +from drf_spectacular.utils import OpenApiParameter, OpenApiTypes, extend_schema from config.swagger import code_response from farm_hub.models import FarmHub @@ -16,6 +16,9 @@ class FarmFeatureAuthorizationView(APIView): @extend_schema( tags=["Access Control"], + parameters=[ + OpenApiParameter(name="farm_uuid", type=OpenApiTypes.UUID, location=OpenApiParameter.PATH, default="11111111-1111-1111-1111-111111111111"), + ], request=FeatureAuthorizationRequestSerializer, responses={200: code_response("FarmFeatureAuthorizationResponse")}, ) diff --git a/config/settings.py b/config/settings.py index d47d81a..409e3bd 100644 --- a/config/settings.py +++ b/config/settings.py @@ -34,7 +34,11 @@ INSTALLED_APPS = [ "crop_zoning", "plant_simulator", "pest_detection", + "weather_forecast.apps.WeatherForecastConfig", "irrigation_recommendation", + "yield_harvest.apps.YieldHarvestConfig", + "economic_overview.apps.EconomicOverviewConfig", + "farm_alerts.apps.FarmAlertsConfig", "fertilization_recommendation", "farm_ai_assistant", "notifications.apps.NotificationsConfig", diff --git a/config/urls.py b/config/urls.py index 85f3b70..0dd9fe5 100644 --- a/config/urls.py +++ b/config/urls.py @@ -18,8 +18,12 @@ urlpatterns = [ path("api/plant-simulator/", include("plant_simulator.urls")), path("api/pest-detection/", include("pest_detection.urls")), path("api/irrigation-recommendation/", include("irrigation_recommendation.urls")), + path("api/weather-forecast/", include("weather_forecast.urls")), + path("api/yield-harvest/", include("yield_harvest.urls")), + path("api/economic-overview/", include("economic_overview.urls")), path("api/fertilization-recommendation/", include("fertilization_recommendation.urls")), path("api/farm-ai-assistant/", include("farm_ai_assistant.urls")), path("api/notifications/", include("notifications.urls")), + path("api/farm-alerts/", include("farm_alerts.urls")), path("api/sensor-external-api/", include("sensor_external_api.urls")), ] diff --git a/crop_zoning/views.py b/crop_zoning/views.py index a13d2c8..8dc936c 100644 --- a/crop_zoning/views.py +++ b/crop_zoning/views.py @@ -32,7 +32,7 @@ AREA_QUERY_PARAMETERS = [ location=OpenApiParameter.QUERY, required=True, description="UUID مزرعه برای گرفتن يا ساخت آخرين پردازش محدوده همان مزرعه.", - ), + default="11111111-1111-1111-1111-111111111111"), OpenApiParameter( name="page", type=OpenApiTypes.INT, diff --git a/dashboard/views.py b/dashboard/views.py index 74ad3ec..9cfe192 100644 --- a/dashboard/views.py +++ b/dashboard/views.py @@ -53,7 +53,7 @@ class FarmAccessMixin: get=extend_schema( tags=["Farm Dashboard"], parameters=[ - OpenApiParameter(name="farm_uuid", type=OpenApiTypes.UUID, location=OpenApiParameter.QUERY, required=True), + OpenApiParameter(name="farm_uuid", type=OpenApiTypes.UUID, location=OpenApiParameter.QUERY, required=True, default="11111111-1111-1111-1111-111111111111"), ], responses={200: code_response("FarmDashboardConfigGetResponse", data=FarmDashboardConfigSerializer())}, ), @@ -109,7 +109,7 @@ class FarmDashboardConfigView(FarmAccessMixin, APIView): get=extend_schema( tags=["Farm Dashboard"], parameters=[ - OpenApiParameter(name="farm_uuid", type=OpenApiTypes.UUID, location=OpenApiParameter.QUERY, required=True), + OpenApiParameter(name="farm_uuid", type=OpenApiTypes.UUID, location=OpenApiParameter.QUERY, required=True, default="11111111-1111-1111-1111-111111111111"), ], responses={200: code_response("FarmDashboardCardsResponse", data=serializers.JSONField())}, ), diff --git a/economic_overview/__init__.py b/economic_overview/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/economic_overview/apps.py b/economic_overview/apps.py new file mode 100644 index 0000000..a46447b --- /dev/null +++ b/economic_overview/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class EconomicOverviewConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "economic_overview" + verbose_name = "Economic Overview" diff --git a/economic_overview/migrations/0001_initial.py b/economic_overview/migrations/0001_initial.py new file mode 100644 index 0000000..afc6aa0 --- /dev/null +++ b/economic_overview/migrations/0001_initial.py @@ -0,0 +1,28 @@ +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("farm_hub", "0001_initial"), + ] + + operations = [ + migrations.CreateModel( + name="EconomicOverviewLog", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("uuid", models.UUIDField(default=uuid.uuid4, editable=False, unique=True, db_index=True)), + ("farm", models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name="economic_overview_logs", to="farm_hub.farmhub")), + ("economic_data", models.JSONField(blank=True, default=list)), + ("chart_series", models.JSONField(blank=True, default=list)), + ("chart_categories", models.JSONField(blank=True, default=list)), + ("fetched_at", models.DateTimeField(auto_now_add=True)), + ], + options={"db_table": "economic_overview_logs", "ordering": ["-fetched_at"]}, + ), + ] diff --git a/economic_overview/migrations/__init__.py b/economic_overview/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/economic_overview/mock_data.py b/economic_overview/mock_data.py new file mode 100644 index 0000000..0e4f857 --- /dev/null +++ b/economic_overview/mock_data.py @@ -0,0 +1,37 @@ +ECONOMIC_OVERVIEW = { + "economicData": [ + { + "title": "هزینه آب", + "value": "€720", + "subtitle": "این ماه", + "avatarIcon": "tabler-droplet", + "avatarColor": "primary", + }, + { + "title": "صرفه‌جویی آب هوشمند", + "value": "€156", + "subtitle": "۱۸٪ صرفه‌جویی شده", + "avatarIcon": "tabler-bulb", + "avatarColor": "success", + }, + { + "title": "بازده سرمایه پلتفرم", + "value": "127%", + "subtitle": "نسبت به سال گذشته", + "avatarIcon": "tabler-chart-line", + "avatarColor": "info", + }, + { + "title": "پیش‌بینی درآمد", + "value": "€42k", + "subtitle": "این فصل", + "avatarIcon": "tabler-currency-euro", + "avatarColor": "success", + }, + ], + "chartSeries": [ + {"name": "هزینه آب", "data": [120, 115, 110, 125, 118, 122]}, + {"name": "کود", "data": [80, 85, 90, 75, 82, 78]}, + ], + "chartCategories": ["ژانویه", "فوریه", "مارس", "آوریل", "می", "ژوئن"], +} diff --git a/economic_overview/models.py b/economic_overview/models.py new file mode 100644 index 0000000..d612961 --- /dev/null +++ b/economic_overview/models.py @@ -0,0 +1,28 @@ +import uuid as uuid_lib + +from django.db import models + +from farm_hub.models import FarmHub + + +class EconomicOverviewLog(models.Model): + uuid = models.UUIDField(default=uuid_lib.uuid4, unique=True, editable=False, db_index=True) + farm = models.ForeignKey( + FarmHub, + on_delete=models.CASCADE, + related_name="economic_overview_logs", + null=True, + blank=True, + ) + economic_data = models.JSONField(default=list, blank=True) + chart_series = models.JSONField(default=list, blank=True) + chart_categories = models.JSONField(default=list, blank=True) + fetched_at = models.DateTimeField(auto_now_add=True) + + class Meta: + db_table = "economic_overview_logs" + ordering = ["-fetched_at"] + + def __str__(self): + farm_label = str(self.farm_id) if self.farm_id else "no-farm" + return f"{farm_label} — {self.fetched_at}" diff --git a/economic_overview/serializers.py b/economic_overview/serializers.py new file mode 100644 index 0000000..2bf7e5c --- /dev/null +++ b/economic_overview/serializers.py @@ -0,0 +1,20 @@ +from rest_framework import serializers + + +class EconomicDataItemSerializer(serializers.Serializer): + title = serializers.CharField() + value = serializers.CharField() + subtitle = serializers.CharField() + avatarIcon = serializers.CharField() + avatarColor = serializers.CharField() + + +class ChartSeriesSerializer(serializers.Serializer): + name = serializers.CharField() + data = serializers.ListField(child=serializers.FloatField()) + + +class EconomicOverviewSerializer(serializers.Serializer): + economicData = EconomicDataItemSerializer(many=True) + chartSeries = ChartSeriesSerializer(many=True) + chartCategories = serializers.ListField(child=serializers.CharField()) diff --git a/economic_overview/urls.py b/economic_overview/urls.py new file mode 100644 index 0000000..172caf8 --- /dev/null +++ b/economic_overview/urls.py @@ -0,0 +1,7 @@ +from django.urls import path + +from .views import EconomicOverviewView + +urlpatterns = [ + path("summary/", EconomicOverviewView.as_view(), name="economic-overview-summary"), +] diff --git a/economic_overview/views.py b/economic_overview/views.py new file mode 100644 index 0000000..1fed30a --- /dev/null +++ b/economic_overview/views.py @@ -0,0 +1,11 @@ +from rest_framework.response import Response +from rest_framework.views import APIView + +from .mock_data import ECONOMIC_OVERVIEW +from .serializers import EconomicOverviewSerializer + + +class EconomicOverviewView(APIView): + def get(self, request): + serializer = EconomicOverviewSerializer(ECONOMIC_OVERVIEW) + return Response({"status": "success", "result": serializer.data}) diff --git a/external_api_adapter/json/ai/index.json b/external_api_adapter/json/ai/index.json index 91c1a8e..3bedd9b 100644 --- a/external_api_adapter/json/ai/index.json +++ b/external_api_adapter/json/ai/index.json @@ -600,5 +600,26 @@ "status_code": 200, "description": "Task status failure", "file": "json/mock_data/tasks/status/get_200_failure.json" + }, + { + "method": "GET", + "path": "/api/pest-detection/risk-summary/", + "status_code": 200, + "description": "Pest and disease risk summary success", + "file": "json/ai/pest-detection/risk-summary/get_200_success.json" + }, + { + "method": "GET", + "path": "/api/weather-forecast/card/", + "status_code": 200, + "description": "Farm weather card data", + "file": "json/ai/weather-forecast/card/get_200_success.json" + }, + { + "method": "GET", + "path": "/api/yield-harvest/summary/", + "status_code": 200, + "description": "Yield prediction card, chart and harvest prediction card", + "file": "json/ai/yield-harvest/summary/get_200_success.json" } -] +] \ No newline at end of file diff --git a/external_api_adapter/json/ai/pest-detection/risk-summary/get_200_success.json b/external_api_adapter/json/ai/pest-detection/risk-summary/get_200_success.json new file mode 100644 index 0000000..e403302 --- /dev/null +++ b/external_api_adapter/json/ai/pest-detection/risk-summary/get_200_success.json @@ -0,0 +1,47 @@ +{ + "status": "success", + "service": "ai", + "path": "/pest-detection/risk-summary", + "result": { + "disease_risk": { + "id": "disease_risk", + "title": "ریسک بیماری", + "subtitle": "۷ روز اخیر", + "stats": "پایین", + "avatarColor": "success", + "avatarIcon": "tabler-bug", + "chipText": "5%", + "chipColor": "success", + "details": { + "risk_level": "low", + "risk_percentage": 5, + "detected_diseases": [], + "last_assessed_at": "2025-07-10T06:00:00Z", + "recommendation": "شرایط فعلی مناسب است. پایش هفتگی توصیه می‌شود." + } + }, + "pest_risk": { + "id": "pest_risk", + "title": "ریسک آفات", + "subtitle": "پیش‌بینی هوشمند", + "stats": "15%", + "avatarColor": "warning", + "avatarIcon": "tabler-bug-off", + "chipText": "تحت نظر", + "chipColor": "warning", + "details": { + "risk_level": "moderate", + "risk_percentage": 15, + "detected_pests": [ + { + "name": "شپشک", + "confidence": 0.72, + "affected_area_percent": 8 + } + ], + "last_assessed_at": "2025-07-10T06:00:00Z", + "recommendation": "بازرسی مزرعه هر ۳ روز یک بار انجام شود. در صورت افزایش، اسپری روغن نیم توصیه می‌شود." + } + } + } +} diff --git a/external_api_adapter/json/ai/weather-forecast/card/get_200_success.json b/external_api_adapter/json/ai/weather-forecast/card/get_200_success.json new file mode 100644 index 0000000..4049557 --- /dev/null +++ b/external_api_adapter/json/ai/weather-forecast/card/get_200_success.json @@ -0,0 +1,17 @@ +{ + "status": "success", + "service": "ai", + "path": "/weather-forecast/card", + "result": { + "condition": "صاف", + "temperature": 24, + "unit": "°C", + "humidity": 45, + "windSpeed": 12, + "windUnit": "km/h", + "chartData": { + "labels": ["۶ صبح", "۹ صبح", "۱۲ ظهر", "۳ بعدازظهر", "۶ عصر", "۹ شب", "۱۲ شب"], + "series": [[18, 22, 26, 28, 25, 20, 18]] + } + } +} diff --git a/external_api_adapter/json/ai/yield-harvest/summary/get_200_success.json b/external_api_adapter/json/ai/yield-harvest/summary/get_200_success.json new file mode 100644 index 0000000..bc1d13b --- /dev/null +++ b/external_api_adapter/json/ai/yield-harvest/summary/get_200_success.json @@ -0,0 +1,51 @@ +{ + "status": "success", + "service": "ai", + "path": "/yield-harvest/summary", + "result": { + "yield_prediction_card": { + "id": "yield_prediction", + "title": "پیش‌بینی عملکرد", + "subtitle": "این فصل", + "stats": "42 تن", + "avatarColor": "secondary", + "avatarIcon": "tabler-chart-bar", + "chipText": "+8%", + "chipColor": "success" + }, + "yield_prediction_chart": { + "categories": [ + "ژانویه", "فوریه", "مارس", "آوریل", "می", "ژوئن", + "ژوئیه", "آگوست", "سپتامبر", "اکتبر", "نوامبر", "دسامبر" + ], + "series": [ + {"name": "امسال", "data": [35, 38, 40, 42, 45, 48, 50, 48, 46, 44, 42, 42]}, + {"name": "سال گذشته", "data": [32, 34, 36, 38, 40, 42, 44, 42, 40, 38, 36, 38]} + ], + "summary": [ + { + "title": "عملکرد پیش‌بینی‌شده", + "subtitle": "این فصل", + "amount": "42 تن", + "avatarColor": "primary", + "avatarIcon": "tabler-chart-bar" + }, + { + "title": "تاریخ برداشت", + "subtitle": "حدود ۱۵ اکتبر", + "amount": "+8%", + "avatarColor": "success", + "avatarIcon": "tabler-calendar" + } + ] + }, + "harvest_prediction_card": { + "date": "2025-10-15", + "dateFormatted": "۱۵ اکتبر ۲۰۲۵", + "daysUntil": 58, + "description": "بر اساس تجمع GDD فعلی و پیش‌بینی آب و هوا. بازه بهینه برداشت: ۱۲ تا ۱۸ اکتبر.", + "optimalWindowStart": "2025-10-12", + "optimalWindowEnd": "2025-10-18" + } + } +} diff --git a/farm_ai_assistant/views.py b/farm_ai_assistant/views.py index 8efec8f..39177e3 100644 --- a/farm_ai_assistant/views.py +++ b/farm_ai_assistant/views.py @@ -46,7 +46,7 @@ class ContextView(FarmAccessMixin, APIView): @extend_schema( tags=["Farm AI Assistant"], parameters=[ - OpenApiParameter(name="farm_uuid", type=OpenApiTypes.UUID, location=OpenApiParameter.QUERY, required=True), + OpenApiParameter(name="farm_uuid", type=OpenApiTypes.UUID, location=OpenApiParameter.QUERY, required=True, default="11111111-1111-1111-1111-111111111111"), ], responses={200: status_response("FarmAiAssistantContextResponse", data=serializers.JSONField())}, ) @@ -329,7 +329,7 @@ class ChatListCreateView(ConversationAccessMixin, APIView): @extend_schema( tags=["Farm AI Assistant"], parameters=[ - OpenApiParameter(name="farm_uuid", type=OpenApiTypes.UUID, location=OpenApiParameter.QUERY, required=True), + OpenApiParameter(name="farm_uuid", type=OpenApiTypes.UUID, location=OpenApiParameter.QUERY, required=True, default="11111111-1111-1111-1111-111111111111"), ], responses={200: status_response("FarmAiAssistantConversationListResponse", data=ConversationSummarySerializer(many=True))}, ) @@ -378,7 +378,7 @@ class ChatMessagesView(ConversationAccessMixin, APIView): tags=["Farm AI Assistant"], parameters=[ OpenApiParameter(name="conversation_id", type=OpenApiTypes.UUID, location=OpenApiParameter.PATH), - OpenApiParameter(name="farm_uuid", type=OpenApiTypes.UUID, location=OpenApiParameter.QUERY, required=True), + OpenApiParameter(name="farm_uuid", type=OpenApiTypes.UUID, location=OpenApiParameter.QUERY, required=True, default="11111111-1111-1111-1111-111111111111"), ], responses={200: status_response("FarmAiAssistantMessageListResponse", data=ConversationMessagesSerializer())}, ) @@ -407,7 +407,7 @@ class ChatDetailView(ConversationAccessMixin, APIView): tags=["Farm AI Assistant"], parameters=[ OpenApiParameter(name="conversation_id", type=OpenApiTypes.UUID, location=OpenApiParameter.PATH), - OpenApiParameter(name="farm_uuid", type=OpenApiTypes.UUID, location=OpenApiParameter.QUERY, required=True), + OpenApiParameter(name="farm_uuid", type=OpenApiTypes.UUID, location=OpenApiParameter.QUERY, required=True, default="11111111-1111-1111-1111-111111111111"), ], responses={200: status_response("FarmAiAssistantConversationDeleteResponse", data=ConversationDeleteSerializer())}, ) @@ -578,7 +578,7 @@ class ChatTaskStatusView(ConversationAccessMixin, APIView): tags=["Farm AI Assistant"], parameters=[ OpenApiParameter(name="task_id", type=OpenApiTypes.STR, location=OpenApiParameter.PATH), - OpenApiParameter(name="farm_uuid", type=OpenApiTypes.UUID, location=OpenApiParameter.QUERY, required=True), + OpenApiParameter(name="farm_uuid", type=OpenApiTypes.UUID, location=OpenApiParameter.QUERY, required=True, default="11111111-1111-1111-1111-111111111111"), ], responses={200: status_response("FarmAiAssistantChatTaskStatusResponse", data=ChatTaskStatusDataSerializer())}, ) diff --git a/farm_alerts/__init__.py b/farm_alerts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/farm_alerts/apps.py b/farm_alerts/apps.py new file mode 100644 index 0000000..4d111cf --- /dev/null +++ b/farm_alerts/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class FarmAlertsConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "farm_alerts" + verbose_name = "Farm Alerts" diff --git a/farm_alerts/migrations/0001_initial.py b/farm_alerts/migrations/0001_initial.py new file mode 100644 index 0000000..3e287a5 --- /dev/null +++ b/farm_alerts/migrations/0001_initial.py @@ -0,0 +1,61 @@ +import uuid + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("farm_hub", "0001_initial"), + ] + + operations = [ + migrations.CreateModel( + name="FarmAlert", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("uuid", models.UUIDField(default=uuid.uuid4, editable=False, unique=True, db_index=True)), + ("farm", models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name="farm_alerts", to="farm_hub.farmhub")), + ("title", models.CharField(max_length=255)), + ("description", models.TextField(blank=True, default="")), + ("color", models.CharField(default="info", max_length=32)), + ("avatar_icon", models.CharField(blank=True, default="", max_length=64)), + ("avatar_color", models.CharField(blank=True, default="", max_length=32)), + ("is_active", models.BooleanField(default=True)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ], + options={"db_table": "farm_alerts", "ordering": ["-created_at"]}, + ), + migrations.CreateModel( + name="AnomalyDetection", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("uuid", models.UUIDField(default=uuid.uuid4, editable=False, unique=True, db_index=True)), + ("farm", models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name="anomalies", to="farm_hub.farmhub")), + ("sensor", models.CharField(max_length=255)), + ("value", models.CharField(max_length=64)), + ("expected", models.CharField(max_length=64)), + ("deviation", models.CharField(max_length=64)), + ("severity", models.CharField(default="warning", max_length=32)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ], + options={"db_table": "farm_anomaly_detections", "ordering": ["-created_at"]}, + ), + migrations.CreateModel( + name="Recommendation", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("uuid", models.UUIDField(default=uuid.uuid4, editable=False, unique=True, db_index=True)), + ("farm", models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name="recommendations", to="farm_hub.farmhub")), + ("title", models.CharField(max_length=255)), + ("subtitle", models.TextField(blank=True, default="")), + ("avatar_icon", models.CharField(blank=True, default="", max_length=64)), + ("avatar_color", models.CharField(blank=True, default="", max_length=32)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ], + options={"db_table": "farm_recommendations", "ordering": ["-created_at"]}, + ), + ] diff --git a/farm_alerts/migrations/__init__.py b/farm_alerts/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/farm_alerts/mock_data.py b/farm_alerts/mock_data.py new file mode 100644 index 0000000..ec7117e --- /dev/null +++ b/farm_alerts/mock_data.py @@ -0,0 +1,101 @@ +ARM_ALERTS_TRACKER = { + "totalAlerts": 3, + "radialBarValue": 30, + "alertStats": [ + { + "title": "کمبود آب", + "count": "2", + "avatarColor": "error", + "avatarIcon": "tabler-droplet-half-2", + }, + { + "title": "ریسک قارچی", + "count": "1", + "avatarColor": "warning", + "avatarIcon": "tabler-mushroom", + }, + { + "title": "هشدار یخبندان", + "count": "0", + "avatarColor": "info", + "avatarIcon": "tabler-snowflake", + }, + ], +} + +FARM_ALERTS_TIMELINE = { + "alerts": [ + { + "title": "ریسک کمبود آب", + "description": "رطوبت خاک در عمق ۱۰ سانتی‌متر (۴۲٪) کمتر از حد بهینه است. پیش‌بینی: در صورت عدم آبیاری، تنش طی ۲ تا ۳ روز. توصیه: آبیاری ظرف ۲۴ ساعت.", + "time": "۱۵ دقیقه پیش", + "color": "warning", + }, + { + "title": "ریسک بیماری قارچی", + "description": "رطوبت بالا (۶۵٪) و دمای ۲۴ درجه شرایط مساعد برای رشد قارچ. استفاده از قارچ‌کش پیشگیرانه یا کاهش آبیاری را در نظر بگیرید.", + "time": "۱ ساعت پیش", + "color": "error", + }, + { + "title": "پیشنهاد آبیاری", + "description": "بازه بهینه آبیاری: ۶:۰۰ تا ۸:۰۰ صبح. حجم پیشنهادی: ۴۵۰ مترمکعب برای زون آ. بهبود راندمان مورد انتظار: ۱۲٪.", + "time": "۲ ساعت پیش", + "color": "info", + }, + { + "title": "بررسی شوری خاک", + "description": "مقدار هدایت الکتریکی ۱/۲ dS/m در محدوده مجاز است. نیازی به اقدام نیست. بررسی بعدی توصیه می‌شود ظرف ۵ روز.", + "time": "۴ ساعت پیش", + "color": "success", + }, + ] +} + +ANOMALY_DETECTION_CARD = { + "anomalies": [ + { + "sensor": "رطوبت خاک زون ۳", + "value": "38%", + "expected": "45-65%", + "deviation": "-12%", + "severity": "warning", + }, + { + "sensor": "pH بخش ۲", + "value": "5.2", + "expected": "6.0-7.0", + "deviation": "-0.8", + "severity": "error", + }, + ] +} + +RECOMMENDATIONS_LIST = { + "recommendations": [ + { + "title": "آبیاری: ۶:۰۰ تا ۸:۰۰ صبح", + "subtitle": "۴۵۰ مترمکعب برای زون آ. بدون آبیاری، عملکرد ممکن است حدود ۸٪ کاهش یابد.", + "avatarIcon": "tabler-droplet", + "avatarColor": "primary", + }, + { + "title": "کود: NPK 20-20-20", + "subtitle": "اعمال ۲۵ کیلوگرم در هکتار ظرف ۷ روز. کمبود نیتروژن فعلی در بخش ۲.", + "avatarIcon": "tabler-leaf", + "avatarColor": "success", + }, + { + "title": "قارچ‌کش: پیشگیرانه", + "subtitle": "رطوبت و دما مساعد قارچ. سمپاشی بر پایه مس را در نظر بگیرید.", + "avatarIcon": "tabler-mushroom", + "avatarColor": "warning", + }, + { + "title": "بازه برداشت: ۱۲ تا ۱۸ اکتبر", + "subtitle": "اوج رسیدگی حدود ۱۵ اکتبر. نیروی کار را متناسب برنامه‌ریزی کنید.", + "avatarIcon": "tabler-calendar-event", + "avatarColor": "info", + }, + ] +} diff --git a/farm_alerts/models.py b/farm_alerts/models.py new file mode 100644 index 0000000..6b3d8da --- /dev/null +++ b/farm_alerts/models.py @@ -0,0 +1,67 @@ +import uuid as uuid_lib + +from django.db import models + +from farm_hub.models import FarmHub + + +SEVERITY_CHOICES = [ + ("info", "Info"), + ("warning", "Warning"), + ("error", "Error"), + ("success", "Success"), +] + + +class FarmAlert(models.Model): + uuid = models.UUIDField(default=uuid_lib.uuid4, unique=True, editable=False, db_index=True) + farm = models.ForeignKey(FarmHub, on_delete=models.CASCADE, related_name="farm_alerts", null=True, blank=True) + title = models.CharField(max_length=255) + description = models.TextField(blank=True, default="") + color = models.CharField(max_length=32, default="info", choices=SEVERITY_CHOICES) + avatar_icon = models.CharField(max_length=64, blank=True, default="") + avatar_color = models.CharField(max_length=32, blank=True, default="") + is_active = models.BooleanField(default=True) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + db_table = "farm_alerts" + ordering = ["-created_at"] + + def __str__(self): + return f"{self.title} ({self.color})" + + +class AnomalyDetection(models.Model): + uuid = models.UUIDField(default=uuid_lib.uuid4, unique=True, editable=False, db_index=True) + farm = models.ForeignKey(FarmHub, on_delete=models.CASCADE, related_name="anomalies", null=True, blank=True) + sensor = models.CharField(max_length=255) + value = models.CharField(max_length=64) + expected = models.CharField(max_length=64) + deviation = models.CharField(max_length=64) + severity = models.CharField(max_length=32, default="warning", choices=SEVERITY_CHOICES) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + db_table = "farm_anomaly_detections" + ordering = ["-created_at"] + + def __str__(self): + return f"{self.sensor}: {self.value}" + + +class Recommendation(models.Model): + uuid = models.UUIDField(default=uuid_lib.uuid4, unique=True, editable=False, db_index=True) + farm = models.ForeignKey(FarmHub, on_delete=models.CASCADE, related_name="recommendations", null=True, blank=True) + title = models.CharField(max_length=255) + subtitle = models.TextField(blank=True, default="") + avatar_icon = models.CharField(max_length=64, blank=True, default="") + avatar_color = models.CharField(max_length=32, blank=True, default="") + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + db_table = "farm_recommendations" + ordering = ["-created_at"] + + def __str__(self): + return self.title diff --git a/farm_alerts/serializers.py b/farm_alerts/serializers.py new file mode 100644 index 0000000..98e3dec --- /dev/null +++ b/farm_alerts/serializers.py @@ -0,0 +1,57 @@ +from rest_framework import serializers + + +class AlertStatSerializer(serializers.Serializer): + title = serializers.CharField() + count = serializers.CharField() + avatarColor = serializers.CharField() + avatarIcon = serializers.CharField() + + +class AlertTrackerSerializer(serializers.Serializer): + totalAlerts = serializers.IntegerField() + radialBarValue = serializers.IntegerField() + alertStats = AlertStatSerializer(many=True) + + +class AlertTimelineItemSerializer(serializers.Serializer): + title = serializers.CharField() + description = serializers.CharField() + time = serializers.CharField() + color = serializers.CharField() + + +class AlertTimelineSerializer(serializers.Serializer): + alerts = AlertTimelineItemSerializer(many=True) + + +class AnomalyItemSerializer(serializers.Serializer): + sensor = serializers.CharField() + value = serializers.CharField() + expected = serializers.CharField() + deviation = serializers.CharField() + severity = serializers.CharField() + + +class AnomalyDetectionSerializer(serializers.Serializer): + anomalies = AnomalyItemSerializer(many=True) + + +class RecommendationItemSerializer(serializers.Serializer): + title = serializers.CharField() + subtitle = serializers.CharField() + avatarIcon = serializers.CharField() + avatarColor = serializers.CharField() + + +class RecommendationsListSerializer(serializers.Serializer): + recommendations = RecommendationItemSerializer(many=True) + + +class CreateAlertSerializer(serializers.Serializer): + farm_uuid = serializers.UUIDField(required=False, allow_null=True) + title = serializers.CharField(max_length=255) + description = serializers.CharField(required=False, default="", allow_blank=True) + color = serializers.ChoiceField(choices=["info", "warning", "error", "success"], default="info") + avatar_icon = serializers.CharField(required=False, default="", allow_blank=True) + avatar_color = serializers.CharField(required=False, default="", allow_blank=True) diff --git a/farm_alerts/services.py b/farm_alerts/services.py new file mode 100644 index 0000000..df8cc94 --- /dev/null +++ b/farm_alerts/services.py @@ -0,0 +1,49 @@ +from farm_hub.models import FarmHub +from notifications.models import FarmNotification + +from .models import FarmAlert + + +class AlertService: + @staticmethod + def create_alert( + title: str, + description: str = "", + color: str = "info", + avatar_icon: str = "", + avatar_color: str = "", + farm_uuid=None, + ) -> FarmAlert: + farm = None + if farm_uuid: + try: + farm = FarmHub.objects.get(uuid=farm_uuid) + except FarmHub.DoesNotExist: + pass + + alert = FarmAlert.objects.create( + farm=farm, + title=title, + description=description, + color=color, + avatar_icon=avatar_icon, + avatar_color=avatar_color, + ) + + AlertService._send_notification(alert, farm) + return alert + + @staticmethod + def _send_notification(alert: FarmAlert, farm) -> None: + if farm is None: + return + + level_map = {"error": "error", "warning": "warning", "info": "info", "success": "success"} + + FarmNotification.objects.create( + farm=farm, + title=alert.title, + message=alert.description, + level=level_map.get(alert.color, "info"), + metadata={"alert_uuid": str(alert.uuid), "color": alert.color}, + ) diff --git a/farm_alerts/urls.py b/farm_alerts/urls.py new file mode 100644 index 0000000..1812267 --- /dev/null +++ b/farm_alerts/urls.py @@ -0,0 +1,17 @@ +from django.urls import path + +from .views import ( + AlertTrackerView, + AlertTimelineView, + AnomalyDetectionView, + RecommendationsListView, + CreateAlertView, +) + +urlpatterns = [ + path("tracker/", AlertTrackerView.as_view(), name="farm-alerts-tracker"), + path("timeline/", AlertTimelineView.as_view(), name="farm-alerts-timeline"), + path("anomalies/", AnomalyDetectionView.as_view(), name="farm-alerts-anomalies"), + path("recommendations/", RecommendationsListView.as_view(), name="farm-alerts-recommendations"), + path("create/", CreateAlertView.as_view(), name="farm-alerts-create"), +] diff --git a/farm_alerts/views.py b/farm_alerts/views.py new file mode 100644 index 0000000..6509d60 --- /dev/null +++ b/farm_alerts/views.py @@ -0,0 +1,80 @@ +from rest_framework import status +from rest_framework.response import Response +from rest_framework.views import APIView + +from farm_hub.models import FarmHub + +from .mock_data import ( + ANOMALY_DETECTION_CARD, + ARM_ALERTS_TRACKER, + FARM_ALERTS_TIMELINE, + RECOMMENDATIONS_LIST, +) +from .serializers import ( + AlertTimelineSerializer, + AlertTrackerSerializer, + AnomalyDetectionSerializer, + CreateAlertSerializer, + RecommendationsListSerializer, +) +from .services import AlertService + + +class AlertTrackerView(APIView): + def get(self, request): + serializer = AlertTrackerSerializer(ARM_ALERTS_TRACKER) + return Response({"status": "success", "result": serializer.data}) + + +class AlertTimelineView(APIView): + def get(self, request): + serializer = AlertTimelineSerializer(FARM_ALERTS_TIMELINE) + return Response({"status": "success", "result": serializer.data}) + + +class AnomalyDetectionView(APIView): + def get(self, request): + serializer = AnomalyDetectionSerializer(ANOMALY_DETECTION_CARD) + return Response({"status": "success", "result": serializer.data}) + + +class RecommendationsListView(APIView): + def get(self, request): + serializer = RecommendationsListSerializer(RECOMMENDATIONS_LIST) + return Response({"status": "success", "result": serializer.data}) + + +class CreateAlertView(APIView): + def post(self, request): + serializer = CreateAlertSerializer(data=request.data) + if not serializer.is_valid(): + return Response( + {"status": "error", "errors": serializer.errors}, + status=status.HTTP_400_BAD_REQUEST, + ) + + data = serializer.validated_data + farm = None + farm_uuid = data.get("farm_uuid") + if farm_uuid: + try: + farm = FarmHub.objects.get(uuid=farm_uuid) + except FarmHub.DoesNotExist: + return Response( + {"status": "error", "message": "farm not found"}, + status=status.HTTP_404_NOT_FOUND, + ) + + alert = AlertService.create_alert( + title=data["title"], + description=data.get("description", ""), + color=data.get("color", "info"), + avatar_icon=data.get("avatar_icon", ""), + avatar_color=data.get("avatar_color", ""), + farm=farm, + ) + + return Response( + {"status": "success", "result": {"uuid": str(alert.uuid), "title": alert.title}}, + status=status.HTTP_201_CREATED, + ) diff --git a/farm_hub/views.py b/farm_hub/views.py index edcd53c..a404093 100644 --- a/farm_hub/views.py +++ b/farm_hub/views.py @@ -3,7 +3,7 @@ from rest_framework import serializers, status from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView -from drf_spectacular.utils import extend_schema +from drf_spectacular.utils import OpenApiParameter, OpenApiTypes, extend_schema from config.swagger import code_response from .models import FarmHub, FarmType, Product @@ -105,6 +105,9 @@ class FarmTypeProductsView(FarmHubBaseView): class FarmDetailView(FarmHubBaseView): @extend_schema( tags=["Farm Hub"], + parameters=[ + OpenApiParameter(name="farm_uuid", type=OpenApiTypes.UUID, location=OpenApiParameter.PATH, default="11111111-1111-1111-1111-111111111111"), + ], responses={ 200: code_response("FarmDetailResponse", data=FarmHubSerializer()), 404: code_response("FarmNotFoundResponse"), @@ -119,6 +122,9 @@ class FarmDetailView(FarmHubBaseView): @extend_schema( tags=["Farm Hub"], + parameters=[ + OpenApiParameter(name="farm_uuid", type=OpenApiTypes.UUID, location=OpenApiParameter.PATH, default="11111111-1111-1111-1111-111111111111"), + ], request=FarmHubCreateSerializer, responses={ 200: code_response("FarmUpdateResponse", data=FarmHubSerializer()), @@ -138,6 +144,9 @@ class FarmDetailView(FarmHubBaseView): @extend_schema( tags=["Farm Hub"], + parameters=[ + OpenApiParameter(name="farm_uuid", type=OpenApiTypes.UUID, location=OpenApiParameter.PATH, default="11111111-1111-1111-1111-111111111111"), + ], responses={ 200: code_response("FarmDeleteResponse"), 404: code_response("FarmDeleteNotFoundResponse"), diff --git a/fertilization_recommendation/views.py b/fertilization_recommendation/views.py index 7fbced1..3d98bd3 100644 --- a/fertilization_recommendation/views.py +++ b/fertilization_recommendation/views.py @@ -36,7 +36,7 @@ class ConfigView(FarmAccessMixin, APIView): @extend_schema( tags=["Fertilization Recommendation"], parameters=[ - OpenApiParameter(name="farm_uuid", type=OpenApiTypes.UUID, location=OpenApiParameter.QUERY, required=True), + OpenApiParameter(name="farm_uuid", type=OpenApiTypes.UUID, location=OpenApiParameter.QUERY, required=True, default="11111111-1111-1111-1111-111111111111"), ], responses={200: status_response("FertilizationConfigResponse", data=serializers.JSONField())}, ) @@ -85,7 +85,7 @@ class RecommendTaskStatusView(FarmAccessMixin, APIView): tags=["Fertilization Recommendation"], parameters=[ OpenApiParameter(name="task_id", type=OpenApiTypes.STR, location=OpenApiParameter.PATH), - OpenApiParameter(name="farm_uuid", type=OpenApiTypes.UUID, location=OpenApiParameter.QUERY, required=True), + OpenApiParameter(name="farm_uuid", type=OpenApiTypes.UUID, location=OpenApiParameter.QUERY, required=True, default="11111111-1111-1111-1111-111111111111"), ], responses={200: status_response("FertilizationRecommendTaskStatusResponse", data=FertilizationTaskStatusDataSerializer())}, ) diff --git a/irrigation_recommendation/views.py b/irrigation_recommendation/views.py index 6e40a1d..83c57ee 100644 --- a/irrigation_recommendation/views.py +++ b/irrigation_recommendation/views.py @@ -36,7 +36,7 @@ class ConfigView(FarmAccessMixin, APIView): @extend_schema( tags=["Irrigation Recommendation"], parameters=[ - OpenApiParameter(name="farm_uuid", type=OpenApiTypes.UUID, location=OpenApiParameter.QUERY, required=True), + OpenApiParameter(name="farm_uuid", type=OpenApiTypes.UUID, location=OpenApiParameter.QUERY, required=True, default="11111111-1111-1111-1111-111111111111"), ], responses={200: status_response("IrrigationConfigResponse", data=serializers.JSONField())}, ) @@ -116,7 +116,7 @@ class RecommendTaskStatusView(FarmAccessMixin, APIView): tags=["Irrigation Recommendation"], parameters=[ OpenApiParameter(name="task_id", type=OpenApiTypes.STR, location=OpenApiParameter.PATH), - OpenApiParameter(name="farm_uuid", type=OpenApiTypes.UUID, location=OpenApiParameter.QUERY, required=True), + OpenApiParameter(name="farm_uuid", type=OpenApiTypes.UUID, location=OpenApiParameter.QUERY, required=True, default="11111111-1111-1111-1111-111111111111"), ], responses={200: status_response("IrrigationRecommendTaskStatusResponse", data=IrrigationTaskStatusDataSerializer())}, ) diff --git a/json/mock_data/dashboard-data/generate/post_202.json b/json/mock_data/dashboard-data/generate/post_202.json deleted file mode 100644 index 34b0f71..0000000 --- a/json/mock_data/dashboard-data/generate/post_202.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "code": 202, - "msg": "dashboard task queued", - "data": { - "task_id": "dashboard-task-123", - "status_url": "/api/dashboard-data/dashboard-task-123/status/" - } -} diff --git a/json/mock_data/dashboard-data/generate/post_400.json b/json/mock_data/dashboard-data/generate/post_400.json deleted file mode 100644 index 5df03b8..0000000 --- a/json/mock_data/dashboard-data/generate/post_400.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": 400, - "msg": "پارامتر sensor_id الزامی است.", - "data": null -} diff --git a/json/mock_data/dashboard-data/status/get_200_failure.json b/json/mock_data/dashboard-data/status/get_200_failure.json deleted file mode 100644 index bc4f2eb..0000000 --- a/json/mock_data/dashboard-data/status/get_200_failure.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "code": 200, - "msg": "success", - "data": { - "task_id": "dashboard-task-123", - "status": "FAILURE", - "error": "خطا در ساخت کارت‌های داشبورد." - } -} diff --git a/json/mock_data/dashboard-data/status/get_200_pending.json b/json/mock_data/dashboard-data/status/get_200_pending.json deleted file mode 100644 index 74dafc6..0000000 --- a/json/mock_data/dashboard-data/status/get_200_pending.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "code": 200, - "msg": "success", - "data": { - "task_id": "dashboard-task-123", - "status": "PENDING", - "message": "تسک در صف یا یافت نشد." - } -} diff --git a/json/mock_data/dashboard-data/status/get_200_progress.json b/json/mock_data/dashboard-data/status/get_200_progress.json deleted file mode 100644 index e3515e5..0000000 --- a/json/mock_data/dashboard-data/status/get_200_progress.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "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/json/mock_data/dashboard-data/status/get_200_success.json b/json/mock_data/dashboard-data/status/get_200_success.json deleted file mode 100644 index fef804e..0000000 --- a/json/mock_data/dashboard-data/status/get_200_success.json +++ /dev/null @@ -1,611 +0,0 @@ -{ - "code": 200, - "msg": "success", - "data": { - "task_id": "dashboard-task-123", - "status": "SUCCESS", - "result": { - "sensor_id": "550e8400-e29b-41d4-a716-446655440000", - "all_cards": { - "farmOverviewKpis": { - "kpis": [ - { - "id": "farm_health_score", - "title": "امتیاز سلامت مزرعه", - "subtitle": "تحلیل هوشمند", - "stats": "87%", - "avatarColor": "success", - "avatarIcon": "tabler-heartbeat", - "chipText": "خوب", - "chipColor": "success" - }, - { - "id": "water_stress_index", - "title": "شاخص تنش آبی", - "subtitle": "فعلی", - "stats": "12%", - "avatarColor": "info", - "avatarIcon": "tabler-droplet", - "chipText": "پایین", - "chipColor": "success" - }, - { - "id": "disease_risk", - "title": "ریسک بیماری", - "subtitle": "۷ روز اخیر", - "stats": "پایین", - "avatarColor": "success", - "avatarIcon": "tabler-bug", - "chipText": "5%", - "chipColor": "success" - }, - { - "id": "avg_soil_moisture", - "title": "میانگین رطوبت خاک", - "subtitle": "کل مزرعه", - "stats": "65%", - "avatarColor": "primary", - "avatarIcon": "tabler-plant-2", - "chipText": "بهینه", - "chipColor": "success" - }, - { - "id": "yield_prediction", - "title": "پیش‌بینی عملکرد", - "subtitle": "این فصل", - "stats": "42 تن", - "avatarColor": "secondary", - "avatarIcon": "tabler-chart-bar", - "chipText": "+8%", - "chipColor": "success" - }, - { - "id": "pest_risk", - "title": "ریسک آفات", - "subtitle": "پیش‌بینی هوشمند", - "stats": "15%", - "avatarColor": "warning", - "avatarIcon": "tabler-bug-off", - "chipText": "تحت نظر", - "chipColor": "warning" - } - ] - }, - "farmWeatherCard": { - "condition": "صاف", - "temperature": 24, - "unit": "°C", - "humidity": 45, - "windSpeed": 12, - "windUnit": "km/h", - "chartData": { - "labels": [ - "۶ صبح", - "۹ صبح", - "۱۲ ظهر", - "۳ بعدازظهر", - "۶ عصر", - "۹ شب", - "۱۲ شب" - ], - "series": [ - [ - 18, - 22, - 26, - 28, - 25, - 20, - 18 - ] - ] - } - }, - "farmAlertsTracker": { - "totalAlerts": 3, - "radialBarValue": 30, - "alertStats": [ - { - "title": "کمبود آب", - "count": "2", - "avatarColor": "error", - "avatarIcon": "tabler-droplet-half-2" - }, - { - "title": "ریسک قارچی", - "count": "1", - "avatarColor": "warning", - "avatarIcon": "tabler-mushroom" - }, - { - "title": "هشدار یخبندان", - "count": "0", - "avatarColor": "info", - "avatarIcon": "tabler-snowflake" - } - ] - }, - "sensorValuesList": { - "sensors": [ - { - "title": "28°C", - "subtitle": "دمای هوا", - "trendNumber": 2.1, - "trend": "positive", - "unit": "°C" - }, - { - "title": "24°C", - "subtitle": "دمای خاک", - "trendNumber": -0.5, - "trend": "negative", - "unit": "°C" - }, - { - "title": "65%", - "subtitle": "رطوبت هوا", - "trendNumber": 3.2, - "trend": "positive", - "unit": "%" - }, - { - "title": "42%", - "subtitle": "رطوبت خاک (۱۰ سانتی‌متر)", - "trendNumber": -1.8, - "trend": "negative", - "unit": "%" - }, - { - "title": "6.8", - "subtitle": "pH خاک", - "trendNumber": 0.2, - "trend": "positive", - "unit": "pH" - }, - { - "title": "1.2", - "subtitle": "هدایت الکتریکی (dS/m)", - "trendNumber": 0.1, - "trend": "positive", - "unit": "dS/m" - }, - { - "title": "850", - "subtitle": "شدت نور (لوکس)", - "trendNumber": 15.3, - "trend": "positive", - "unit": "lux" - }, - { - "title": "12", - "subtitle": "سرعت باد (کیلومتر/ساعت)", - "trendNumber": -2.4, - "trend": "negative", - "unit": "km/h" - } - ] - }, - "sensorRadarChart": { - "labels": [ - "دما", - "رطوبت", - "pH", - "هدایت الکتریکی", - "نور", - "باد" - ], - "series": [ - { - "name": "امروز", - "data": [ - 75, - 65, - 80, - 70, - 85, - 60 - ] - }, - { - "name": "ایده‌آل", - "data": [ - 80, - 70, - 75, - 75, - 90, - 50 - ] - } - ] - }, - "sensorComparisonChart": { - "currentValue": 48, - "vsLastWeek": "+5%", - "vsLastWeekValue": 5, - "categories": [ - "دوشنبه", - "سه‌شنبه", - "چهارشنبه", - "پنج‌شنبه", - "جمعه", - "شنبه", - "یکشنبه" - ], - "series": [ - { - "name": "امروز", - "data": [ - 42, - 45, - 48, - 52, - 50, - 48, - 46 - ] - }, - { - "name": "هفته قبل", - "data": [ - 38, - 40, - 42, - 45, - 43, - 40, - 38 - ] - } - ] - }, - "anomalyDetectionCard": { - "anomalies": [ - { - "sensor": "رطوبت خاک زون ۳", - "value": "38%", - "expected": "45-65%", - "deviation": "-12%", - "severity": "warning" - }, - { - "sensor": "pH بخش ۲", - "value": "5.2", - "expected": "6.0-7.0", - "deviation": "-0.8", - "severity": "error" - } - ] - }, - "farmAlertsTimeline": { - "alerts": [ - { - "title": "ریسک کمبود آب", - "description": "رطوبت خاک در عمق ۱۰ سانتی‌متر (۴۲٪) کمتر از حد بهینه است. پیش‌بینی: در صورت عدم آبیاری، تنش طی ۲ تا ۳ روز. توصیه: آبیاری ظرف ۲۴ ساعت.", - "time": "۱۵ دقیقه پیش", - "color": "warning" - }, - { - "title": "ریسک بیماری قارچی", - "description": "رطوبت بالا (۶۵٪) و دمای ۲۴ درجه شرایط مساعد برای رشد قارچ. استفاده از قارچ‌کش پیشگیرانه یا کاهش آبیاری را در نظر بگیرید.", - "time": "۱ ساعت پیش", - "color": "error" - }, - { - "title": "پیشنهاد آبیاری", - "description": "بازه بهینه آبیاری: ۶:۰۰ تا ۸:۰۰ صبح. حجم پیشنهادی: ۴۵۰ مترمکعب برای زون آ. بهبود راندمان مورد انتظار: ۱۲٪.", - "time": "۲ ساعت پیش", - "color": "info" - }, - { - "title": "بررسی شوری خاک", - "description": "مقدار هدایت الکتریکی ۱/۲ dS/m در محدوده مجاز است. نیازی به اقدام نیست. بررسی بعدی توصیه می‌شود ظرف ۵ روز.", - "time": "۴ ساعت پیش", - "color": "success" - } - ] - }, - "waterNeedPrediction": { - "totalNext7Days": 3290, - "unit": "m³", - "categories": [ - "روز ۱", - "روز ۲", - "روز ۳", - "روز ۴", - "روز ۵", - "روز ۶", - "روز ۷" - ], - "series": [ - { - "name": "نیاز آبی", - "data": [ - 420, - 450, - 480, - 460, - 490, - 510, - 480 - ] - } - ] - }, - "harvestPredictionCard": { - "date": "2025-10-15", - "dateFormatted": "۱۵ اکتبر ۲۰۲۵", - "daysUntil": 58, - "description": "بر اساس تجمع GDD فعلی و پیش‌بینی آب و هوا. بازه بهینه برداشت: ۱۲ تا ۱۸ اکتبر.", - "optimalWindowStart": "2025-10-12", - "optimalWindowEnd": "2025-10-18" - }, - "yieldPredictionChart": { - "categories": [ - "ژانویه", - "فوریه", - "مارس", - "آوریل", - "می", - "ژوئن", - "ژوئیه", - "آگوست", - "سپتامبر", - "اکتبر", - "نوامبر", - "دسامبر" - ], - "series": [ - { - "name": "امسال", - "data": [ - 35, - 38, - 40, - 42, - 45, - 48, - 50, - 48, - 46, - 44, - 42, - 42 - ] - }, - { - "name": "سال گذشته", - "data": [ - 32, - 34, - 36, - 38, - 40, - 42, - 44, - 42, - 40, - 38, - 36, - 38 - ] - } - ], - "summary": [ - { - "title": "عملکرد پیش‌بینی‌شده", - "subtitle": "این فصل", - "amount": "42 تن", - "avatarColor": "primary", - "avatarIcon": "tabler-chart-bar" - }, - { - "title": "تاریخ برداشت", - "subtitle": "حدود ۱۵ اکتبر", - "amount": "+8%", - "avatarColor": "success", - "avatarIcon": "tabler-calendar" - } - ] - }, - "soilMoistureHeatmap": { - "zones": [ - "زون ۱", - "زون ۲", - "زون ۳", - "زون ۴", - "زون ۵", - "زون ۶", - "زون ۷" - ], - "hours": [ - "۶ ص", - "۸ ص", - "۱۰ ص", - "۱۲ ظ", - "۱۴ ع", - "۱۶ ع", - "۱۸ ع" - ], - "series": [ - { - "name": "زون ۱", - "data": [ - { - "x": "۶ ص", - "y": 52 - }, - { - "x": "۸ ص", - "y": 48 - }, - { - "x": "۱۰ ص", - "y": 55 - }, - { - "x": "۱۲ ظ", - "y": 60 - }, - { - "x": "۱۴ ع", - "y": 58 - }, - { - "x": "۱۶ ع", - "y": 54 - }, - { - "x": "۱۸ ع", - "y": 50 - } - ] - }, - { - "name": "زون ۲", - "data": [ - { - "x": "۶ ص", - "y": 45 - }, - { - "x": "۸ ص", - "y": 42 - }, - { - "x": "۱۰ ص", - "y": 48 - }, - { - "x": "۱۲ ظ", - "y": 52 - }, - { - "x": "۱۴ ع", - "y": 50 - }, - { - "x": "۱۶ ع", - "y": 47 - }, - { - "x": "۱۸ ع", - "y": 44 - } - ] - } - ] - }, - "ndviHealthCard": { - "ndviIndex": 0.78, - "healthData": [ - { - "title": "تنش نیتروژن", - "value": "پایین", - "color": "success", - "icon": "tabler-leaf" - }, - { - "title": "سلامت محصول", - "value": "خوب", - "color": "success", - "icon": "tabler-plant" - } - ] - }, - "recommendationsList": { - "recommendations": [ - { - "title": "آبیاری: ۶:۰۰ تا ۸:۰۰ صبح", - "subtitle": "۴۵۰ مترمکعب برای زون آ. بدون آبیاری، عملکرد ممکن است حدود ۸٪ کاهش یابد.", - "avatarIcon": "tabler-droplet", - "avatarColor": "primary" - }, - { - "title": "کود: NPK 20-20-20", - "subtitle": "اعمال ۲۵ کیلوگرم در هکتار ظرف ۷ روز. کمبود نیتروژن فعلی در بخش ۲.", - "avatarIcon": "tabler-leaf", - "avatarColor": "success" - }, - { - "title": "قارچ‌کش: پیشگیرانه", - "subtitle": "رطوبت و دما مساعد قارچ. سمپاشی بر پایه مس را در نظر بگیرید.", - "avatarIcon": "tabler-mushroom", - "avatarColor": "warning" - }, - { - "title": "بازه برداشت: ۱۲ تا ۱۸ اکتبر", - "subtitle": "اوج رسیدگی حدود ۱۵ اکتبر. نیروی کار را متناسب برنامه‌ریزی کنید.", - "avatarIcon": "tabler-calendar-event", - "avatarColor": "info" - } - ] - }, - "economicOverview": { - "economicData": [ - { - "title": "هزینه آب", - "value": "€720", - "subtitle": "این ماه", - "avatarIcon": "tabler-droplet", - "avatarColor": "primary" - }, - { - "title": "صرفه‌جویی آب هوشمند", - "value": "€156", - "subtitle": "۱۸٪ صرفه‌جویی شده", - "avatarIcon": "tabler-bulb", - "avatarColor": "success" - }, - { - "title": "بازده سرمایه پلتفرم", - "value": "127%", - "subtitle": "نسبت به سال گذشته", - "avatarIcon": "tabler-chart-line", - "avatarColor": "info" - }, - { - "title": "پیش‌بینی درآمد", - "value": "€42k", - "subtitle": "این فصل", - "avatarIcon": "tabler-currency-euro", - "avatarColor": "success" - } - ], - "chartSeries": [ - { - "name": "هزینه آب", - "data": [ - 120, - 115, - 110, - 125, - 118, - 122 - ] - }, - { - "name": "کود", - "data": [ - 80, - 85, - 90, - 75, - 82, - 78 - ] - } - ], - "chartCategories": [ - "ژانویه", - "فوریه", - "مارس", - "آوریل", - "می", - "ژوئن" - ] - } - } - } - } -} diff --git a/json/mock_data/fertilization/recommend/post_202.json b/json/mock_data/fertilization/recommend/post_202.json deleted file mode 100644 index e8dc23f..0000000 --- a/json/mock_data/fertilization/recommend/post_202.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "code": 202, - "msg": "تسک توصیه کودهی در صف قرار گرفت.", - "data": { - "task_id": "fert-task-123", - "status_url": "/api/fertilization/recommend/fert-task-123/status/" - } -} diff --git a/json/mock_data/fertilization/recommend/post_400.json b/json/mock_data/fertilization/recommend/post_400.json deleted file mode 100644 index 5e4c3f5..0000000 --- a/json/mock_data/fertilization/recommend/post_400.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "code": 400, - "msg": "داده نامعتبر.", - "data": { - "farm_uuid": [ - "This field is required." - ] - } -} diff --git a/json/mock_data/fertilization/status/get_200_failure.json b/json/mock_data/fertilization/status/get_200_failure.json deleted file mode 100644 index fb8bad0..0000000 --- a/json/mock_data/fertilization/status/get_200_failure.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "code": 200, - "msg": "success", - "data": { - "task_id": "fert-task-123", - "status": "FAILURE", - "error": "خطا در دریافت توصیه کودهی." - } -} diff --git a/json/mock_data/fertilization/status/get_200_pending.json b/json/mock_data/fertilization/status/get_200_pending.json deleted file mode 100644 index 110be1a..0000000 --- a/json/mock_data/fertilization/status/get_200_pending.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "code": 200, - "msg": "success", - "data": { - "task_id": "fert-task-123", - "status": "PENDING", - "message": "تسک در صف یا یافت نشد." - } -} diff --git a/json/mock_data/fertilization/status/get_200_progress.json b/json/mock_data/fertilization/status/get_200_progress.json deleted file mode 100644 index ffdf909..0000000 --- a/json/mock_data/fertilization/status/get_200_progress.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "code": 200, - "msg": "success", - "data": { - "task_id": "fert-task-123", - "status": "PROGRESS", - "progress": { - "message": "در حال پردازش توصیه کودهی..." - } - } -} diff --git a/json/mock_data/fertilization/status/get_200_success.json b/json/mock_data/fertilization/status/get_200_success.json deleted file mode 100644 index 7b46ba1..0000000 --- a/json/mock_data/fertilization/status/get_200_success.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "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/json/mock_data/index.json b/json/mock_data/index.json deleted file mode 100644 index 91c1a8e..0000000 --- a/json/mock_data/index.json +++ /dev/null @@ -1,604 +0,0 @@ -[ - { - "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/{farm_uuid}/", - "status_code": 200, - "description": "Sensor update put success", - "file": "json/mock_data/sensor-data/update-put_200.json" - }, - { - "method": "PUT", - "path": "/api/sensor-data/{farm_uuid}/", - "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/{farm_uuid}/", - "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/{farm_uuid}/", - "status_code": 200, - "description": "Sensor update patch success", - "file": "json/mock_data/sensor-data/update-patch_200.json" - }, - { - "method": "PATCH", - "path": "/api/sensor-data/{farm_uuid}/", - "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/{farm_uuid}/", - "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/json/mock_data/irrigation/method-detail/delete_200.json b/json/mock_data/irrigation/method-detail/delete_200.json deleted file mode 100644 index ed52092..0000000 --- a/json/mock_data/irrigation/method-detail/delete_200.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": 200, - "msg": "روش آبیاری با موفقیت حذف شد.", - "data": null -} diff --git a/json/mock_data/irrigation/method-detail/delete_404.json b/json/mock_data/irrigation/method-detail/delete_404.json deleted file mode 100644 index 54dcfff..0000000 --- a/json/mock_data/irrigation/method-detail/delete_404.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": 404, - "msg": "روش آبیاری یافت نشد.", - "data": null -} diff --git a/json/mock_data/irrigation/method-detail/get_200.json b/json/mock_data/irrigation/method-detail/get_200.json deleted file mode 100644 index 5988c67..0000000 --- a/json/mock_data/irrigation/method-detail/get_200.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "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/json/mock_data/irrigation/method-detail/get_404.json b/json/mock_data/irrigation/method-detail/get_404.json deleted file mode 100644 index 54dcfff..0000000 --- a/json/mock_data/irrigation/method-detail/get_404.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": 404, - "msg": "روش آبیاری یافت نشد.", - "data": null -} diff --git a/json/mock_data/irrigation/method-detail/patch_200.json b/json/mock_data/irrigation/method-detail/patch_200.json deleted file mode 100644 index 5988c67..0000000 --- a/json/mock_data/irrigation/method-detail/patch_200.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "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/json/mock_data/irrigation/method-detail/patch_400.json b/json/mock_data/irrigation/method-detail/patch_400.json deleted file mode 100644 index 2e4dd22..0000000 --- a/json/mock_data/irrigation/method-detail/patch_400.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "code": 400, - "msg": "داده نامعتبر.", - "data": { - "name": [ - "This field may not be blank." - ] - } -} diff --git a/json/mock_data/irrigation/method-detail/patch_404.json b/json/mock_data/irrigation/method-detail/patch_404.json deleted file mode 100644 index 54dcfff..0000000 --- a/json/mock_data/irrigation/method-detail/patch_404.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": 404, - "msg": "روش آبیاری یافت نشد.", - "data": null -} diff --git a/json/mock_data/irrigation/method-detail/put_200.json b/json/mock_data/irrigation/method-detail/put_200.json deleted file mode 100644 index 5988c67..0000000 --- a/json/mock_data/irrigation/method-detail/put_200.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "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/json/mock_data/irrigation/method-detail/put_400.json b/json/mock_data/irrigation/method-detail/put_400.json deleted file mode 100644 index 2e4dd22..0000000 --- a/json/mock_data/irrigation/method-detail/put_400.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "code": 400, - "msg": "داده نامعتبر.", - "data": { - "name": [ - "This field may not be blank." - ] - } -} diff --git a/json/mock_data/irrigation/method-detail/put_404.json b/json/mock_data/irrigation/method-detail/put_404.json deleted file mode 100644 index 54dcfff..0000000 --- a/json/mock_data/irrigation/method-detail/put_404.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": 404, - "msg": "روش آبیاری یافت نشد.", - "data": null -} diff --git a/json/mock_data/irrigation/methods/get_200.json b/json/mock_data/irrigation/methods/get_200.json deleted file mode 100644 index a2e511e..0000000 --- a/json/mock_data/irrigation/methods/get_200.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "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/json/mock_data/irrigation/methods/post_201.json b/json/mock_data/irrigation/methods/post_201.json deleted file mode 100644 index 2fba4f5..0000000 --- a/json/mock_data/irrigation/methods/post_201.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "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/json/mock_data/irrigation/methods/post_400.json b/json/mock_data/irrigation/methods/post_400.json deleted file mode 100644 index f72f28c..0000000 --- a/json/mock_data/irrigation/methods/post_400.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "code": 400, - "msg": "داده نامعتبر.", - "data": { - "name": [ - "This field is required." - ] - } -} diff --git a/json/mock_data/irrigation/recommend/post_202.json b/json/mock_data/irrigation/recommend/post_202.json deleted file mode 100644 index a5f230d..0000000 --- a/json/mock_data/irrigation/recommend/post_202.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "code": 202, - "msg": "تسک توصیه آبیاری در صف قرار گرفت.", - "data": { - "task_id": "irr-task-123", - "status_url": "/api/irrigation/recommend/irr-task-123/status/" - } -} diff --git a/json/mock_data/irrigation/recommend/post_400.json b/json/mock_data/irrigation/recommend/post_400.json deleted file mode 100644 index 5e4c3f5..0000000 --- a/json/mock_data/irrigation/recommend/post_400.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "code": 400, - "msg": "داده نامعتبر.", - "data": { - "farm_uuid": [ - "This field is required." - ] - } -} diff --git a/json/mock_data/irrigation/recommend/status/get_200_failure.json b/json/mock_data/irrigation/recommend/status/get_200_failure.json deleted file mode 100644 index 5f4e1f7..0000000 --- a/json/mock_data/irrigation/recommend/status/get_200_failure.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "code": 200, - "msg": "success", - "data": { - "task_id": "irr-task-123", - "status": "FAILURE", - "error": "خطا در دریافت توصیه آبیاری." - } -} diff --git a/json/mock_data/irrigation/recommend/status/get_200_pending.json b/json/mock_data/irrigation/recommend/status/get_200_pending.json deleted file mode 100644 index 498b382..0000000 --- a/json/mock_data/irrigation/recommend/status/get_200_pending.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "code": 200, - "msg": "success", - "data": { - "task_id": "irr-task-123", - "status": "PENDING", - "message": "تسک در صف یا یافت نشد." - } -} diff --git a/json/mock_data/irrigation/recommend/status/get_200_progress.json b/json/mock_data/irrigation/recommend/status/get_200_progress.json deleted file mode 100644 index e8837dd..0000000 --- a/json/mock_data/irrigation/recommend/status/get_200_progress.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "code": 200, - "msg": "success", - "data": { - "task_id": "irr-task-123", - "status": "PROGRESS", - "progress": { - "message": "در حال پردازش توصیه آبیاری..." - } - } -} diff --git a/json/mock_data/irrigation/recommend/status/get_200_success.json b/json/mock_data/irrigation/recommend/status/get_200_success.json deleted file mode 100644 index 46f9c90..0000000 --- a/json/mock_data/irrigation/recommend/status/get_200_success.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "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/json/mock_data/plant/create-post_201.json b/json/mock_data/plant/create-post_201.json deleted file mode 100644 index 32c4614..0000000 --- a/json/mock_data/plant/create-post_201.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "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/json/mock_data/plant/create-post_400.json b/json/mock_data/plant/create-post_400.json deleted file mode 100644 index f72f28c..0000000 --- a/json/mock_data/plant/create-post_400.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "code": 400, - "msg": "داده نامعتبر.", - "data": { - "name": [ - "This field is required." - ] - } -} diff --git a/json/mock_data/plant/detail-delete_200.json b/json/mock_data/plant/detail-delete_200.json deleted file mode 100644 index b127160..0000000 --- a/json/mock_data/plant/detail-delete_200.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": 200, - "msg": "گیاه با موفقیت حذف شد.", - "data": null -} diff --git a/json/mock_data/plant/detail-delete_404.json b/json/mock_data/plant/detail-delete_404.json deleted file mode 100644 index 497519d..0000000 --- a/json/mock_data/plant/detail-delete_404.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": 404, - "msg": "گیاه یافت نشد.", - "data": null -} diff --git a/json/mock_data/plant/detail-get_200.json b/json/mock_data/plant/detail-get_200.json deleted file mode 100644 index af5f8ad..0000000 --- a/json/mock_data/plant/detail-get_200.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "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/json/mock_data/plant/detail-get_404.json b/json/mock_data/plant/detail-get_404.json deleted file mode 100644 index 497519d..0000000 --- a/json/mock_data/plant/detail-get_404.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": 404, - "msg": "گیاه یافت نشد.", - "data": null -} diff --git a/json/mock_data/plant/detail-patch_200.json b/json/mock_data/plant/detail-patch_200.json deleted file mode 100644 index af5f8ad..0000000 --- a/json/mock_data/plant/detail-patch_200.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "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/json/mock_data/plant/detail-patch_400.json b/json/mock_data/plant/detail-patch_400.json deleted file mode 100644 index 2e4dd22..0000000 --- a/json/mock_data/plant/detail-patch_400.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "code": 400, - "msg": "داده نامعتبر.", - "data": { - "name": [ - "This field may not be blank." - ] - } -} diff --git a/json/mock_data/plant/detail-patch_404.json b/json/mock_data/plant/detail-patch_404.json deleted file mode 100644 index 497519d..0000000 --- a/json/mock_data/plant/detail-patch_404.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": 404, - "msg": "گیاه یافت نشد.", - "data": null -} diff --git a/json/mock_data/plant/detail-put_200.json b/json/mock_data/plant/detail-put_200.json deleted file mode 100644 index af5f8ad..0000000 --- a/json/mock_data/plant/detail-put_200.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "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/json/mock_data/plant/detail-put_400.json b/json/mock_data/plant/detail-put_400.json deleted file mode 100644 index 2e4dd22..0000000 --- a/json/mock_data/plant/detail-put_400.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "code": 400, - "msg": "داده نامعتبر.", - "data": { - "name": [ - "This field may not be blank." - ] - } -} diff --git a/json/mock_data/plant/detail-put_404.json b/json/mock_data/plant/detail-put_404.json deleted file mode 100644 index 497519d..0000000 --- a/json/mock_data/plant/detail-put_404.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": 404, - "msg": "گیاه یافت نشد.", - "data": null -} diff --git a/json/mock_data/plant/fetch-info-post_200.json b/json/mock_data/plant/fetch-info-post_200.json deleted file mode 100644 index af5f8ad..0000000 --- a/json/mock_data/plant/fetch-info-post_200.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "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/json/mock_data/plant/fetch-info-post_400.json b/json/mock_data/plant/fetch-info-post_400.json deleted file mode 100644 index e4bbdd2..0000000 --- a/json/mock_data/plant/fetch-info-post_400.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": 400, - "msg": "نام گیاه الزامی است.", - "data": null -} diff --git a/json/mock_data/plant/fetch-info-post_503.json b/json/mock_data/plant/fetch-info-post_503.json deleted file mode 100644 index f4911a7..0000000 --- a/json/mock_data/plant/fetch-info-post_503.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": 503, - "msg": "سرویس API هنوز پیاده‌سازی نشده است.", - "data": null -} diff --git a/json/mock_data/plant/list-get_200.json b/json/mock_data/plant/list-get_200.json deleted file mode 100644 index e6586a2..0000000 --- a/json/mock_data/plant/list-get_200.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "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/json/mock_data/rag/chat-post_200_stream.json b/json/mock_data/rag/chat-post_200_stream.json deleted file mode 100644 index 2847fcd..0000000 --- a/json/mock_data/rag/chat-post_200_stream.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "content": "Here is the recommended plan.", - "sections": [ - { - "type": "recommendation", - "title": "Irrigation Plan", - "icon": "droplet", - "frequency": "3 times per week", - "amount": "15 liters per plant", - "timing": "Early morning", - "expandableExplanation": "Loamy soil holds moisture well, so moderate frequency is enough." - }, - { - "type": "list", - "title": "Important Notes", - "icon": "leaf", - "items": [ - "Avoid watering at noon", - "Check leaf stress every two days" - ] - }, - { - "type": "warning", - "title": "Heat Alert", - "icon": "warning", - "content": "Increase irrigation if temperature rises above 35°C." - } - ] -} diff --git a/json/mock_data/rag/chat-post_400_invalid_service.json b/json/mock_data/rag/chat-post_400_invalid_service.json deleted file mode 100644 index c484816..0000000 --- a/json/mock_data/rag/chat-post_400_invalid_service.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "code": 400, - "msg": "service_id نامعتبر است: unknown_service" -} diff --git a/json/mock_data/rag/chat-post_400_missing_query.json b/json/mock_data/rag/chat-post_400_missing_query.json deleted file mode 100644 index b491272..0000000 --- a/json/mock_data/rag/chat-post_400_missing_query.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "code": 400, - "msg": "پارامتر query الزامی است." -} diff --git a/json/mock_data/rag/chat-post_400_missing_user.json b/json/mock_data/rag/chat-post_400_missing_user.json deleted file mode 100644 index 3615785..0000000 --- a/json/mock_data/rag/chat-post_400_missing_user.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "code": 400, - "msg": "برای این service_id، پارامتر user_id الزامی است." -} diff --git a/json/mock_data/rag/fertilization/post_202.json b/json/mock_data/rag/fertilization/post_202.json deleted file mode 100644 index 8fe7cc7..0000000 --- a/json/mock_data/rag/fertilization/post_202.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "code": 202, - "msg": "تسک توصیه کودهی در صف قرار گرفت.", - "data": { - "task_id": "rag-fert-123", - "status_url": "/api/rag/recommend/fertilization/rag-fert-123/status/" - } -} diff --git a/json/mock_data/rag/fertilization/post_400.json b/json/mock_data/rag/fertilization/post_400.json deleted file mode 100644 index b82ea08..0000000 --- a/json/mock_data/rag/fertilization/post_400.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": 400, - "msg": "پارامتر farm_uuid الزامی است.", - "data": null -} diff --git a/json/mock_data/rag/fertilization/status/get_200_failure.json b/json/mock_data/rag/fertilization/status/get_200_failure.json deleted file mode 100644 index 971dd04..0000000 --- a/json/mock_data/rag/fertilization/status/get_200_failure.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "code": 200, - "msg": "success", - "data": { - "task_id": "rag-fert-123", - "status": "FAILURE", - "error": "خطا در دریافت توصیه کودهی." - } -} diff --git a/json/mock_data/rag/fertilization/status/get_200_pending.json b/json/mock_data/rag/fertilization/status/get_200_pending.json deleted file mode 100644 index 9c9eca6..0000000 --- a/json/mock_data/rag/fertilization/status/get_200_pending.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "code": 200, - "msg": "success", - "data": { - "task_id": "rag-fert-123", - "status": "PENDING", - "message": "تسک در صف یا یافت نشد." - } -} diff --git a/json/mock_data/rag/fertilization/status/get_200_progress.json b/json/mock_data/rag/fertilization/status/get_200_progress.json deleted file mode 100644 index 4cd6d57..0000000 --- a/json/mock_data/rag/fertilization/status/get_200_progress.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "code": 200, - "msg": "success", - "data": { - "task_id": "rag-fert-123", - "status": "PROGRESS", - "progress": { - "message": "در حال پردازش توصیه کودهی..." - } - } -} diff --git a/json/mock_data/rag/fertilization/status/get_200_success.json b/json/mock_data/rag/fertilization/status/get_200_success.json deleted file mode 100644 index cea132a..0000000 --- a/json/mock_data/rag/fertilization/status/get_200_success.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "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/json/mock_data/rag/irrigation/post_202.json b/json/mock_data/rag/irrigation/post_202.json deleted file mode 100644 index bcc6db9..0000000 --- a/json/mock_data/rag/irrigation/post_202.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "code": 202, - "msg": "تسک توصیه آبیاری در صف قرار گرفت.", - "data": { - "task_id": "rag-irr-123", - "status_url": "/api/rag/recommend/irrigation/rag-irr-123/status/" - } -} diff --git a/json/mock_data/rag/irrigation/post_400.json b/json/mock_data/rag/irrigation/post_400.json deleted file mode 100644 index b82ea08..0000000 --- a/json/mock_data/rag/irrigation/post_400.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": 400, - "msg": "پارامتر farm_uuid الزامی است.", - "data": null -} diff --git a/json/mock_data/rag/irrigation/status/get_200_failure.json b/json/mock_data/rag/irrigation/status/get_200_failure.json deleted file mode 100644 index bffa098..0000000 --- a/json/mock_data/rag/irrigation/status/get_200_failure.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "code": 200, - "msg": "success", - "data": { - "task_id": "rag-irr-123", - "status": "FAILURE", - "error": "خطا در دریافت توصیه آبیاری." - } -} diff --git a/json/mock_data/rag/irrigation/status/get_200_pending.json b/json/mock_data/rag/irrigation/status/get_200_pending.json deleted file mode 100644 index 1074e86..0000000 --- a/json/mock_data/rag/irrigation/status/get_200_pending.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "code": 200, - "msg": "success", - "data": { - "task_id": "rag-irr-123", - "status": "PENDING", - "message": "تسک در صف یا یافت نشد." - } -} diff --git a/json/mock_data/rag/irrigation/status/get_200_progress.json b/json/mock_data/rag/irrigation/status/get_200_progress.json deleted file mode 100644 index 3b88990..0000000 --- a/json/mock_data/rag/irrigation/status/get_200_progress.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "code": 200, - "msg": "success", - "data": { - "task_id": "rag-irr-123", - "status": "PROGRESS", - "progress": { - "message": "در حال پردازش توصیه آبیاری..." - } - } -} diff --git a/json/mock_data/rag/irrigation/status/get_200_success.json b/json/mock_data/rag/irrigation/status/get_200_success.json deleted file mode 100644 index 5679648..0000000 --- a/json/mock_data/rag/irrigation/status/get_200_success.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "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/json/mock_data/sensor-data/parameters-post_201.json b/json/mock_data/sensor-data/parameters-post_201.json deleted file mode 100644 index 9de5e9d..0000000 --- a/json/mock_data/sensor-data/parameters-post_201.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "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/json/mock_data/sensor-data/parameters-post_400.json b/json/mock_data/sensor-data/parameters-post_400.json deleted file mode 100644 index 98b89aa..0000000 --- a/json/mock_data/sensor-data/parameters-post_400.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "code": 400, - "msg": "داده نامعتبر.", - "data": { - "code": [ - "This field is required." - ] - } -} diff --git a/json/mock_data/sensor-data/update-patch_200.json b/json/mock_data/sensor-data/update-patch_200.json deleted file mode 100644 index 31fc9cd..0000000 --- a/json/mock_data/sensor-data/update-patch_200.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "code": 200, - "msg": "success", - "data": { - "farm_uuid": "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/json/mock_data/sensor-data/update-patch_400.json b/json/mock_data/sensor-data/update-patch_400.json deleted file mode 100644 index cf343c5..0000000 --- a/json/mock_data/sensor-data/update-patch_400.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "code": 400, - "msg": "داده نامعتبر.", - "data": { - "location_id": [ - "This field is required." - ] - } -} diff --git a/json/mock_data/sensor-data/update-patch_404.json b/json/mock_data/sensor-data/update-patch_404.json deleted file mode 100644 index 107ea33..0000000 --- a/json/mock_data/sensor-data/update-patch_404.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": 404, - "msg": "location_id یافت نشد.", - "data": null -} diff --git a/json/mock_data/sensor-data/update-put_200.json b/json/mock_data/sensor-data/update-put_200.json deleted file mode 100644 index 31fc9cd..0000000 --- a/json/mock_data/sensor-data/update-put_200.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "code": 200, - "msg": "success", - "data": { - "farm_uuid": "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/json/mock_data/sensor-data/update-put_400.json b/json/mock_data/sensor-data/update-put_400.json deleted file mode 100644 index cf343c5..0000000 --- a/json/mock_data/sensor-data/update-put_400.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "code": 400, - "msg": "داده نامعتبر.", - "data": { - "location_id": [ - "This field is required." - ] - } -} diff --git a/json/mock_data/sensor-data/update-put_404.json b/json/mock_data/sensor-data/update-put_404.json deleted file mode 100644 index 107ea33..0000000 --- a/json/mock_data/sensor-data/update-put_404.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": 404, - "msg": "location_id یافت نشد.", - "data": null -} diff --git a/json/mock_data/soil-data/get_200_database.json b/json/mock_data/soil-data/get_200_database.json deleted file mode 100644 index 87becb1..0000000 --- a/json/mock_data/soil-data/get_200_database.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "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/json/mock_data/soil-data/get_202_queued.json b/json/mock_data/soil-data/get_202_queued.json deleted file mode 100644 index 5a06843..0000000 --- a/json/mock_data/soil-data/get_202_queued.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "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/json/mock_data/soil-data/get_400.json b/json/mock_data/soil-data/get_400.json deleted file mode 100644 index bfc19ab..0000000 --- a/json/mock_data/soil-data/get_400.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "code": 400, - "msg": "داده نامعتبر.", - "data": { - "lat": [ - "This field is required." - ], - "lon": [ - "This field is required." - ] - } -} diff --git a/json/mock_data/soil-data/post_200_database.json b/json/mock_data/soil-data/post_200_database.json deleted file mode 100644 index 87becb1..0000000 --- a/json/mock_data/soil-data/post_200_database.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "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/json/mock_data/soil-data/post_202_queued.json b/json/mock_data/soil-data/post_202_queued.json deleted file mode 100644 index 5a06843..0000000 --- a/json/mock_data/soil-data/post_202_queued.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "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/json/mock_data/soil-data/post_400.json b/json/mock_data/soil-data/post_400.json deleted file mode 100644 index 4d32daf..0000000 --- a/json/mock_data/soil-data/post_400.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "code": 400, - "msg": "داده نامعتبر.", - "data": { - "lat": [ - "A valid number is required." - ] - } -} diff --git a/json/mock_data/soil-data/status/get_200_failure.json b/json/mock_data/soil-data/status/get_200_failure.json deleted file mode 100644 index a0c0697..0000000 --- a/json/mock_data/soil-data/status/get_200_failure.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "code": 200, - "msg": "success", - "data": { - "task_id": "soil-task-123", - "status": "FAILURE", - "error": "خطا در واکشی داده خاک." - } -} diff --git a/json/mock_data/soil-data/status/get_200_pending.json b/json/mock_data/soil-data/status/get_200_pending.json deleted file mode 100644 index b4965ce..0000000 --- a/json/mock_data/soil-data/status/get_200_pending.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "code": 200, - "msg": "success", - "data": { - "task_id": "soil-task-123", - "status": "PENDING", - "message": "تسک در صف یا یافت نشد." - } -} diff --git a/json/mock_data/soil-data/status/get_200_progress.json b/json/mock_data/soil-data/status/get_200_progress.json deleted file mode 100644 index 29ecfe5..0000000 --- a/json/mock_data/soil-data/status/get_200_progress.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "code": 200, - "msg": "success", - "data": { - "task_id": "soil-task-123", - "status": "PROGRESS", - "progress": { - "step": "fetch", - "percent": 60 - } - } -} diff --git a/json/mock_data/soil-data/status/get_200_success.json b/json/mock_data/soil-data/status/get_200_success.json deleted file mode 100644 index 04d0827..0000000 --- a/json/mock_data/soil-data/status/get_200_success.json +++ /dev/null @@ -1,67 +0,0 @@ -{ - "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/json/mock_data/tasks/post_200.json b/json/mock_data/tasks/post_200.json deleted file mode 100644 index 8fc9fe2..0000000 --- a/json/mock_data/tasks/post_200.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "code": 200, - "msg": "success", - "data": { - "task_id": "sample-task-123" - } -} diff --git a/json/mock_data/tasks/status/get_200_failure.json b/json/mock_data/tasks/status/get_200_failure.json deleted file mode 100644 index c524d68..0000000 --- a/json/mock_data/tasks/status/get_200_failure.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "code": 200, - "msg": "success", - "data": { - "task_id": "farm-ai-chat-task-123", - "status": "FAILURE", - "error": "Sample task failed." - } -} diff --git a/json/mock_data/tasks/status/get_200_pending.json b/json/mock_data/tasks/status/get_200_pending.json deleted file mode 100644 index 5470c7d..0000000 --- a/json/mock_data/tasks/status/get_200_pending.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "code": 200, - "msg": "success", - "data": { - "task_id": "farm-ai-chat-task-123", - "status": "PENDING", - "message": "تسک در صف یا یافت نشد." - } -} diff --git a/json/mock_data/tasks/status/get_200_progress.json b/json/mock_data/tasks/status/get_200_progress.json deleted file mode 100644 index 0a3107e..0000000 --- a/json/mock_data/tasks/status/get_200_progress.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "code": 200, - "msg": "success", - "data": { - "task_id": "farm-ai-chat-task-123", - "status": "PROGRESS", - "progress": { - "current": 1, - "total": 3, - "message": "در حال پردازش..." - } - } -} diff --git a/json/mock_data/tasks/status/get_200_success.json b/json/mock_data/tasks/status/get_200_success.json deleted file mode 100644 index c5b78bd..0000000 --- a/json/mock_data/tasks/status/get_200_success.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "code": 200, - "msg": "success", - "data": { - "task_id": "farm-ai-chat-task-123", - "status": "SUCCESS", - "result": { - "$ref": "rag/chat-post_200_stream.json" - } - } -} diff --git a/notifications/views.py b/notifications/views.py index 66e2cf7..63d5370 100644 --- a/notifications/views.py +++ b/notifications/views.py @@ -14,13 +14,13 @@ from .services import long_poll_notifications, mark_notifications_as_read class NotificationLongPollQuerySerializer(serializers.Serializer): - farm_uuid = serializers.UUIDField() + farm_uuid = serializers.UUIDField(default="11111111-1111-1111-1111-111111111111") since_id = serializers.IntegerField(required=False, min_value=1) timeout = serializers.IntegerField(required=False, min_value=0, max_value=60) class NotificationListQuerySerializer(serializers.Serializer): - farm_uuid = serializers.UUIDField() + farm_uuid = serializers.UUIDField(default="11111111-1111-1111-1111-111111111111") page = serializers.IntegerField(required=False, min_value=1) page_size = serializers.IntegerField(required=False, min_value=1, max_value=100) @@ -32,7 +32,7 @@ class NotificationPagination(PageNumberPagination): class NotificationMarkReadSerializer(serializers.Serializer): - farm_uuid = serializers.UUIDField() + farm_uuid = serializers.UUIDField(default="11111111-1111-1111-1111-111111111111") slice_id = serializers.IntegerField(min_value=1) @@ -158,4 +158,3 @@ class NotificationMarkReadView(APIView): {"code": 200, "msg": "success", "marked_count": marked_count}, status=status.HTTP_200_OK, ) - diff --git a/pest_detection/mock_data.py b/pest_detection/mock_data.py index 5eb3a4d..2ea4815 100644 --- a/pest_detection/mock_data.py +++ b/pest_detection/mock_data.py @@ -1,6 +1,6 @@ """ Static mock data for Pest Detection API. -No database, no dynamic values. Used for analyze endpoint response. +No database, no dynamic values. Used for analyze and risk-summary endpoint responses. """ ANALYZE_RESPONSE_DATA = { @@ -9,3 +9,46 @@ ANALYZE_RESPONSE_DATA = { "description": "حشرات کوچک مکنده شیره که باعث پیچ خوردگی برگ می‌شوند.", "treatment": "یک بار در هفته از اسپری روغن نیم استفاده کنید.", } + +RISK_SUMMARY_RESPONSE_DATA = { + "disease_risk": { + "id": "disease_risk", + "title": "ریسک بیماری", + "subtitle": "۷ روز اخیر", + "stats": "پایین", + "avatarColor": "success", + "avatarIcon": "tabler-bug", + "chipText": "5%", + "chipColor": "success", + "details": { + "risk_level": "low", + "risk_percentage": 5, + "detected_diseases": [], + "last_assessed_at": "2025-07-10T06:00:00Z", + "recommendation": "شرایط فعلی مناسب است. پایش هفتگی توصیه می‌شود.", + }, + }, + "pest_risk": { + "id": "pest_risk", + "title": "ریسک آفات", + "subtitle": "پیش‌بینی هوشمند", + "stats": "15%", + "avatarColor": "warning", + "avatarIcon": "tabler-bug-off", + "chipText": "تحت نظر", + "chipColor": "warning", + "details": { + "risk_level": "moderate", + "risk_percentage": 15, + "detected_pests": [ + { + "name": "شپشک", + "confidence": 0.72, + "affected_area_percent": 8, + } + ], + "last_assessed_at": "2025-07-10T06:00:00Z", + "recommendation": "بازرسی مزرعه هر ۳ روز یک بار انجام شود. در صورت افزایش، اسپری روغن نیم توصیه می‌شود.", + }, + }, +} diff --git a/pest_detection/serializers.py b/pest_detection/serializers.py new file mode 100644 index 0000000..66b84b8 --- /dev/null +++ b/pest_detection/serializers.py @@ -0,0 +1,27 @@ +from rest_framework import serializers + + +class RiskDetailsSerializer(serializers.Serializer): + risk_level = serializers.CharField(required=False, allow_blank=True) + risk_percentage = serializers.IntegerField(required=False) + detected_diseases = serializers.ListField(child=serializers.DictField(), required=False) + detected_pests = serializers.ListField(child=serializers.DictField(), required=False) + last_assessed_at = serializers.CharField(required=False, allow_blank=True) + recommendation = serializers.CharField(required=False, allow_blank=True) + + +class RiskCardSerializer(serializers.Serializer): + id = serializers.CharField(required=False, allow_blank=True) + title = serializers.CharField(required=False, allow_blank=True) + subtitle = serializers.CharField(required=False, allow_blank=True) + stats = serializers.CharField(required=False, allow_blank=True) + avatarColor = serializers.CharField(required=False, allow_blank=True) + avatarIcon = serializers.CharField(required=False, allow_blank=True) + chipText = serializers.CharField(required=False, allow_blank=True) + chipColor = serializers.CharField(required=False, allow_blank=True) + details = RiskDetailsSerializer(required=False) + + +class RiskSummaryDataSerializer(serializers.Serializer): + disease_risk = RiskCardSerializer(required=False) + pest_risk = RiskCardSerializer(required=False) diff --git a/pest_detection/urls.py b/pest_detection/urls.py index 2cf1705..044d959 100644 --- a/pest_detection/urls.py +++ b/pest_detection/urls.py @@ -1,7 +1,8 @@ from django.urls import path -from .views import AnalyzeView +from .views import AnalyzeView, RiskSummaryView urlpatterns = [ path("analyze/", AnalyzeView.as_view(), name="pest-detection-analyze"), + path("risk-summary/", RiskSummaryView.as_view(), name="pest-detection-risk-summary"), ] diff --git a/pest_detection/views.py b/pest_detection/views.py index 2003848..3a1ab55 100644 --- a/pest_detection/views.py +++ b/pest_detection/views.py @@ -9,10 +9,12 @@ from rest_framework import serializers, status from rest_framework.response import Response from rest_framework.views import APIView from drf_spectacular.types import OpenApiTypes -from drf_spectacular.utils import extend_schema +from drf_spectacular.utils import OpenApiParameter, extend_schema from config.swagger import status_response +from external_api_adapter import request as external_api_request from .mock_data import ANALYZE_RESPONSE_DATA +from .serializers import RiskSummaryDataSerializer class AnalyzeView(APIView): @@ -46,3 +48,54 @@ class AnalyzeView(APIView): {"status": "success", "data": ANALYZE_RESPONSE_DATA}, status=status.HTTP_200_OK, ) + + +class RiskSummaryView(APIView): + """ + GET endpoint for combined pest and disease risk summary. + + Purpose: + Returns disease_risk and pest_risk card data for the farm dashboard. + Calls the AI external adapter for live/mock risk assessment results. + + Input parameters: + - farm_uuid (query, optional): UUID of the farm to assess. + + Response structure: + - status: string, always "success". + - data: object with keys disease_risk and pest_risk, + each containing card display fields (id, title, subtitle, stats, + avatarColor, avatarIcon, chipText, chipColor) and a details object. + """ + + @extend_schema( + tags=["Pest Detection"], + parameters=[ + OpenApiParameter( + name="farm_uuid", + type=OpenApiTypes.UUID, + location=OpenApiParameter.QUERY, + required=False, + description="UUID of the farm for risk assessment.", + default="11111111-1111-1111-1111-111111111111"), + ], + responses={200: status_response("PestDetectionRiskSummaryResponse", data=RiskSummaryDataSerializer())}, + ) + def get(self, request): + farm_uuid = request.query_params.get("farm_uuid") + query = {"farm_uuid": str(farm_uuid)} if farm_uuid else {} + + adapter_response = external_api_request( + "ai", + "/pest-detection/risk-summary", + method="GET", + query=query, + ) + + response_data = adapter_response.data if isinstance(adapter_response.data, dict) else {} + result = response_data.get("result", response_data.get("data", response_data)) + + return Response( + {"status": "success", "data": result}, + status=status.HTTP_200_OK, + ) diff --git a/sensor_external_api/views.py b/sensor_external_api/views.py index b634b92..e0f0358 100644 --- a/sensor_external_api/views.py +++ b/sensor_external_api/views.py @@ -92,7 +92,7 @@ class SensorExternalRequestLogListAPIView(APIView): @extend_schema( tags=["Sensor External API"], parameters=[ - OpenApiParameter(name="farm_uuid", type=OpenApiTypes.UUID, location=OpenApiParameter.QUERY, required=True), + OpenApiParameter(name="farm_uuid", type=OpenApiTypes.UUID, location=OpenApiParameter.QUERY, required=True, default="11111111-1111-1111-1111-111111111111"), OpenApiParameter(name="page", type=OpenApiTypes.INT, location=OpenApiParameter.QUERY, required=False), OpenApiParameter(name="page_size", type=OpenApiTypes.INT, location=OpenApiParameter.QUERY, required=False), diff --git a/weather_forecast/__init__.py b/weather_forecast/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/weather_forecast/apps.py b/weather_forecast/apps.py new file mode 100644 index 0000000..287508c --- /dev/null +++ b/weather_forecast/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class WeatherForecastConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "weather_forecast" + verbose_name = "Weather Forecast" diff --git a/weather_forecast/migrations/0001_initial.py b/weather_forecast/migrations/0001_initial.py new file mode 100644 index 0000000..409ca24 --- /dev/null +++ b/weather_forecast/migrations/0001_initial.py @@ -0,0 +1,43 @@ +import django.db.models.deletion +import uuid +from django.db import migrations, models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [ + ("farm_hub", "0007_farmhub_subscription_plan"), + ] + + operations = [ + migrations.CreateModel( + name="WeatherForecastLog", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("uuid", models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, unique=True)), + ("condition", models.CharField(blank=True, default="", max_length=128)), + ("temperature", models.FloatField(blank=True, null=True)), + ("unit", models.CharField(blank=True, default="°C", max_length=16)), + ("humidity", models.IntegerField(blank=True, null=True)), + ("wind_speed", models.FloatField(blank=True, null=True)), + ("wind_unit", models.CharField(blank=True, default="km/h", max_length=16)), + ("chart_data", models.JSONField(blank=True, default=dict)), + ("fetched_at", models.DateTimeField(auto_now_add=True)), + ( + "farm", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="weather_forecasts", + to="farm_hub.farmhub", + ), + ), + ], + options={ + "db_table": "weather_forecast_logs", + "ordering": ["-fetched_at"], + }, + ), + ] diff --git a/weather_forecast/migrations/__init__.py b/weather_forecast/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/weather_forecast/mock_data.py b/weather_forecast/mock_data.py new file mode 100644 index 0000000..08757e8 --- /dev/null +++ b/weather_forecast/mock_data.py @@ -0,0 +1,17 @@ +""" +Static mock data for Weather Forecast API. +Mirrors the farmWeatherCard dashboard card shape. +""" + +FARM_WEATHER_CARD = { + "condition": "صاف", + "temperature": 24, + "unit": "°C", + "humidity": 45, + "windSpeed": 12, + "windUnit": "km/h", + "chartData": { + "labels": ["۶ صبح", "۹ صبح", "۱۲ ظهر", "۳ بعدازظهر", "۶ عصر", "۹ شب", "۱۲ شب"], + "series": [[18, 22, 26, 28, 25, 20, 18]], + }, +} diff --git a/weather_forecast/models.py b/weather_forecast/models.py new file mode 100644 index 0000000..ffe2435 --- /dev/null +++ b/weather_forecast/models.py @@ -0,0 +1,32 @@ +import uuid as uuid_lib + +from django.db import models + +from farm_hub.models import FarmHub + + +class WeatherForecastLog(models.Model): + uuid = models.UUIDField(default=uuid_lib.uuid4, unique=True, editable=False, db_index=True) + farm = models.ForeignKey( + FarmHub, + on_delete=models.CASCADE, + related_name="weather_forecasts", + null=True, + blank=True, + ) + condition = models.CharField(max_length=128, blank=True, default="") + temperature = models.FloatField(null=True, blank=True) + unit = models.CharField(max_length=16, blank=True, default="°C") + humidity = models.IntegerField(null=True, blank=True) + wind_speed = models.FloatField(null=True, blank=True) + wind_unit = models.CharField(max_length=16, blank=True, default="km/h") + chart_data = models.JSONField(default=dict, blank=True) + fetched_at = models.DateTimeField(auto_now_add=True) + + class Meta: + db_table = "weather_forecast_logs" + ordering = ["-fetched_at"] + + def __str__(self): + farm_label = str(self.farm_id) if self.farm_id else "no-farm" + return f"{farm_label} — {self.condition} {self.temperature}{self.unit}" diff --git a/weather_forecast/serializers.py b/weather_forecast/serializers.py new file mode 100644 index 0000000..86c5190 --- /dev/null +++ b/weather_forecast/serializers.py @@ -0,0 +1,19 @@ +from rest_framework import serializers + + +class WeatherChartDataSerializer(serializers.Serializer): + labels = serializers.ListField(child=serializers.CharField(), required=False) + series = serializers.ListField( + child=serializers.ListField(child=serializers.FloatField()), + required=False, + ) + + +class FarmWeatherCardSerializer(serializers.Serializer): + condition = serializers.CharField(required=False, allow_blank=True) + temperature = serializers.FloatField(required=False) + unit = serializers.CharField(required=False, allow_blank=True) + humidity = serializers.IntegerField(required=False) + windSpeed = serializers.FloatField(required=False) + windUnit = serializers.CharField(required=False, allow_blank=True) + chartData = WeatherChartDataSerializer(required=False) diff --git a/weather_forecast/urls.py b/weather_forecast/urls.py new file mode 100644 index 0000000..728f643 --- /dev/null +++ b/weather_forecast/urls.py @@ -0,0 +1,7 @@ +from django.urls import path + +from .views import FarmWeatherCardView + +urlpatterns = [ + path("card/", FarmWeatherCardView.as_view(), name="weather-forecast-card"), +] diff --git a/weather_forecast/views.py b/weather_forecast/views.py new file mode 100644 index 0000000..eab89f5 --- /dev/null +++ b/weather_forecast/views.py @@ -0,0 +1,92 @@ +""" +Weather Forecast API views. +Response format: {"status": "success", "data": }. HTTP 200 only. +Fetches weather card data from the AI external adapter and persists a log entry +if a valid farm_uuid is provided. +""" + +from rest_framework import serializers, status +from rest_framework.response import Response +from rest_framework.views import APIView +from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import OpenApiParameter, extend_schema + +from config.swagger import status_response +from external_api_adapter import request as external_api_request +from farm_hub.models import FarmHub +from .models import WeatherForecastLog +from .serializers import FarmWeatherCardSerializer + + +class FarmWeatherCardView(APIView): + """ + GET endpoint for the farm weather card dashboard data. + + Purpose: + Returns current weather conditions and an intraday temperature chart + for a given farm. Data is fetched from the AI external adapter. + If farm_uuid is provided and the farm exists, the result is persisted + in WeatherForecastLog for historical reference. + + Input parameters: + - farm_uuid (query, optional): UUID of the farm. + + Response structure: + - status: string, always "success". + - data: object matching the farmWeatherCard shape — condition, + temperature, unit, humidity, windSpeed, windUnit, chartData. + """ + + @extend_schema( + tags=["Weather Forecast"], + parameters=[ + OpenApiParameter( + name="farm_uuid", + type=OpenApiTypes.UUID, + location=OpenApiParameter.QUERY, + required=False, + description="UUID of the farm to fetch weather data for.", + default="11111111-1111-1111-1111-111111111111"), + ], + responses={200: status_response("FarmWeatherCardResponse", data=FarmWeatherCardSerializer())}, + ) + def get(self, request): + farm_uuid = request.query_params.get("farm_uuid") + query = {"farm_uuid": str(farm_uuid)} if farm_uuid else {} + + adapter_response = external_api_request( + "ai", + "/weather-forecast/card", + method="GET", + query=query, + ) + + response_data = adapter_response.data if isinstance(adapter_response.data, dict) else {} + card_data = response_data.get("result", response_data.get("data", response_data)) + + self._persist_log(farm_uuid, card_data) + + return Response( + {"status": "success", "data": card_data}, + status=status.HTTP_200_OK, + ) + + @staticmethod + def _persist_log(farm_uuid, card_data): + farm = None + if farm_uuid: + try: + farm = FarmHub.objects.get(farm_uuid=farm_uuid) + except (FarmHub.DoesNotExist, Exception): + pass + + WeatherForecastLog.objects.create( + farm=farm, + condition=card_data.get("condition", ""), + temperature=card_data.get("temperature"), + unit=card_data.get("unit", "°C"), + humidity=card_data.get("humidity"), + wind_speed=card_data.get("windSpeed"), + wind_unit=card_data.get("windUnit", "km/h"), + chart_data=card_data.get("chartData", {}), + ) diff --git a/yield_harvest/__init__.py b/yield_harvest/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/yield_harvest/apps.py b/yield_harvest/apps.py new file mode 100644 index 0000000..5bb08ac --- /dev/null +++ b/yield_harvest/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class YieldHarvestConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "yield_harvest" + verbose_name = "Yield & Harvest Prediction" diff --git a/yield_harvest/migrations/0001_initial.py b/yield_harvest/migrations/0001_initial.py new file mode 100644 index 0000000..f6fa871 --- /dev/null +++ b/yield_harvest/migrations/0001_initial.py @@ -0,0 +1,43 @@ +import django.db.models.deletion +import uuid +from django.db import migrations, models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [ + ("farm_hub", "0007_farmhub_subscription_plan"), + ] + + operations = [ + migrations.CreateModel( + name="YieldHarvestPredictionLog", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("uuid", models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, unique=True)), + ("yield_stats", models.CharField(blank=True, default="", max_length=64)), + ("yield_chip_text", models.CharField(blank=True, default="", max_length=32)), + ("harvest_date", models.DateField(blank=True, null=True)), + ("days_until_harvest", models.IntegerField(blank=True, null=True)), + ("optimal_window_start", models.DateField(blank=True, null=True)), + ("optimal_window_end", models.DateField(blank=True, null=True)), + ("chart_data", models.JSONField(blank=True, default=dict)), + ("fetched_at", models.DateTimeField(auto_now_add=True)), + ( + "farm", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="yield_harvest_predictions", + to="farm_hub.farmhub", + ), + ), + ], + options={ + "db_table": "yield_harvest_prediction_logs", + "ordering": ["-fetched_at"], + }, + ), + ] diff --git a/yield_harvest/migrations/__init__.py b/yield_harvest/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/yield_harvest/mock_data.py b/yield_harvest/mock_data.py new file mode 100644 index 0000000..4abfbcf --- /dev/null +++ b/yield_harvest/mock_data.py @@ -0,0 +1,51 @@ +""" +Static mock data for Yield & Harvest Prediction API. +Mirrors the yieldPredictionChart and harvestPredictionCard dashboard card shapes. +""" + +YIELD_PREDICTION_CARD = { + "id": "yield_prediction", + "title": "پیش‌بینی عملکرد", + "subtitle": "این فصل", + "stats": "42 تن", + "avatarColor": "secondary", + "avatarIcon": "tabler-chart-bar", + "chipText": "+8%", + "chipColor": "success", +} + +YIELD_PREDICTION_CHART = { + "categories": [ + "ژانویه", "فوریه", "مارس", "آوریل", "می", "ژوئن", + "ژوئیه", "آگوست", "سپتامبر", "اکتبر", "نوامبر", "دسامبر", + ], + "series": [ + {"name": "امسال", "data": [35, 38, 40, 42, 45, 48, 50, 48, 46, 44, 42, 42]}, + {"name": "سال گذشته", "data": [32, 34, 36, 38, 40, 42, 44, 42, 40, 38, 36, 38]}, + ], + "summary": [ + { + "title": "عملکرد پیش‌بینی‌شده", + "subtitle": "این فصل", + "amount": "42 تن", + "avatarColor": "primary", + "avatarIcon": "tabler-chart-bar", + }, + { + "title": "تاریخ برداشت", + "subtitle": "حدود ۱۵ اکتبر", + "amount": "+8%", + "avatarColor": "success", + "avatarIcon": "tabler-calendar", + }, + ], +} + +HARVEST_PREDICTION_CARD = { + "date": "2025-10-15", + "dateFormatted": "۱۵ اکتبر ۲۰۲۵", + "daysUntil": 58, + "description": "بر اساس تجمع GDD فعلی و پیش‌بینی آب و هوا. بازه بهینه برداشت: ۱۲ تا ۱۸ اکتبر.", + "optimalWindowStart": "2025-10-12", + "optimalWindowEnd": "2025-10-18", +} diff --git a/yield_harvest/models.py b/yield_harvest/models.py new file mode 100644 index 0000000..632c331 --- /dev/null +++ b/yield_harvest/models.py @@ -0,0 +1,32 @@ +import uuid as uuid_lib + +from django.db import models + +from farm_hub.models import FarmHub + + +class YieldHarvestPredictionLog(models.Model): + uuid = models.UUIDField(default=uuid_lib.uuid4, unique=True, editable=False, db_index=True) + farm = models.ForeignKey( + FarmHub, + on_delete=models.CASCADE, + related_name="yield_harvest_predictions", + null=True, + blank=True, + ) + yield_stats = models.CharField(max_length=64, blank=True, default="") + yield_chip_text = models.CharField(max_length=32, blank=True, default="") + harvest_date = models.DateField(null=True, blank=True) + days_until_harvest = models.IntegerField(null=True, blank=True) + optimal_window_start = models.DateField(null=True, blank=True) + optimal_window_end = models.DateField(null=True, blank=True) + chart_data = models.JSONField(default=dict, blank=True) + fetched_at = models.DateTimeField(auto_now_add=True) + + class Meta: + db_table = "yield_harvest_prediction_logs" + ordering = ["-fetched_at"] + + def __str__(self): + farm_label = str(self.farm_id) if self.farm_id else "no-farm" + return f"{farm_label} — {self.yield_stats} harvest:{self.harvest_date}" diff --git a/yield_harvest/serializers.py b/yield_harvest/serializers.py new file mode 100644 index 0000000..c32076c --- /dev/null +++ b/yield_harvest/serializers.py @@ -0,0 +1,46 @@ +from rest_framework import serializers + + +class YieldPredictionCardSerializer(serializers.Serializer): + id = serializers.CharField(required=False, allow_blank=True) + title = serializers.CharField(required=False, allow_blank=True) + subtitle = serializers.CharField(required=False, allow_blank=True) + stats = serializers.CharField(required=False, allow_blank=True) + avatarColor = serializers.CharField(required=False, allow_blank=True) + avatarIcon = serializers.CharField(required=False, allow_blank=True) + chipText = serializers.CharField(required=False, allow_blank=True) + chipColor = serializers.CharField(required=False, allow_blank=True) + + +class ChartSeriesSerializer(serializers.Serializer): + name = serializers.CharField(required=False, allow_blank=True) + data = serializers.ListField(child=serializers.FloatField(), required=False) + + +class ChartSummaryItemSerializer(serializers.Serializer): + title = serializers.CharField(required=False, allow_blank=True) + subtitle = serializers.CharField(required=False, allow_blank=True) + amount = serializers.CharField(required=False, allow_blank=True) + avatarColor = serializers.CharField(required=False, allow_blank=True) + avatarIcon = serializers.CharField(required=False, allow_blank=True) + + +class YieldPredictionChartSerializer(serializers.Serializer): + categories = serializers.ListField(child=serializers.CharField(), required=False) + series = ChartSeriesSerializer(many=True, required=False) + summary = ChartSummaryItemSerializer(many=True, required=False) + + +class HarvestPredictionCardSerializer(serializers.Serializer): + date = serializers.CharField(required=False, allow_blank=True) + dateFormatted = serializers.CharField(required=False, allow_blank=True) + daysUntil = serializers.IntegerField(required=False) + description = serializers.CharField(required=False, allow_blank=True) + optimalWindowStart = serializers.CharField(required=False, allow_blank=True) + optimalWindowEnd = serializers.CharField(required=False, allow_blank=True) + + +class YieldHarvestSummarySerializer(serializers.Serializer): + yield_prediction_card = YieldPredictionCardSerializer(required=False) + yield_prediction_chart = YieldPredictionChartSerializer(required=False) + harvest_prediction_card = HarvestPredictionCardSerializer(required=False) diff --git a/yield_harvest/urls.py b/yield_harvest/urls.py new file mode 100644 index 0000000..99f8486 --- /dev/null +++ b/yield_harvest/urls.py @@ -0,0 +1,7 @@ +from django.urls import path + +from .views import YieldHarvestSummaryView + +urlpatterns = [ + path("summary/", YieldHarvestSummaryView.as_view(), name="yield-harvest-summary"), +] diff --git a/yield_harvest/views.py b/yield_harvest/views.py new file mode 100644 index 0000000..da77de4 --- /dev/null +++ b/yield_harvest/views.py @@ -0,0 +1,98 @@ +""" +Yield & Harvest Prediction API views. +Response format: {"status": "success", "data": }. HTTP 200 only. +Fetches all three prediction payloads (yield card, yield chart, harvest card) +from the AI external adapter in a single call and persists a log entry +if a valid farm_uuid is provided. +""" + +from rest_framework import status +from rest_framework.response import Response +from rest_framework.views import APIView +from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import OpenApiParameter, extend_schema + +from config.swagger import status_response +from external_api_adapter import request as external_api_request +from farm_hub.models import FarmHub +from .models import YieldHarvestPredictionLog +from .serializers import YieldHarvestSummarySerializer + + +class YieldHarvestSummaryView(APIView): + """ + GET endpoint for combined yield prediction and harvest prediction data. + + Purpose: + Returns three dashboard card payloads in one response: + - yield_prediction_card (kpi card shape) + - yield_prediction_chart (monthly chart + summary) + - harvest_prediction_card (harvest date + window) + Data is fetched from the AI external adapter. If farm_uuid is provided + and the farm exists, the result is persisted in YieldHarvestPredictionLog. + + Input parameters: + - farm_uuid (query, optional): UUID of the farm. + + Response structure: + - status: string, always "success". + - data: object with keys yield_prediction_card, + yield_prediction_chart, harvest_prediction_card. + """ + + @extend_schema( + tags=["Yield & Harvest Prediction"], + parameters=[ + OpenApiParameter( + name="farm_uuid", + type=OpenApiTypes.UUID, + location=OpenApiParameter.QUERY, + required=False, + description="UUID of the farm for yield and harvest prediction.", + default="11111111-1111-1111-1111-111111111111"), + ], + responses={200: status_response("YieldHarvestSummaryResponse", data=YieldHarvestSummarySerializer())}, + ) + def get(self, request): + farm_uuid = request.query_params.get("farm_uuid") + query = {"farm_uuid": str(farm_uuid)} if farm_uuid else {} + + adapter_response = external_api_request( + "ai", + "/yield-harvest/summary", + method="GET", + query=query, + ) + + response_data = adapter_response.data if isinstance(adapter_response.data, dict) else {} + summary = response_data.get("result", response_data.get("data", response_data)) + + self._persist_log(farm_uuid, summary) + + return Response( + {"status": "success", "data": summary}, + status=status.HTTP_200_OK, + ) + + @staticmethod + def _persist_log(farm_uuid, summary): + farm = None + if farm_uuid: + try: + farm = FarmHub.objects.get(farm_uuid=farm_uuid) + except (FarmHub.DoesNotExist, Exception): + pass + + yield_card = summary.get("yield_prediction_card", {}) + harvest_card = summary.get("harvest_prediction_card", {}) + + YieldHarvestPredictionLog.objects.create( + farm=farm, + yield_stats=yield_card.get("stats", ""), + yield_chip_text=yield_card.get("chipText", ""), + harvest_date=harvest_card.get("date") or None, + days_until_harvest=harvest_card.get("daysUntil"), + optimal_window_start=harvest_card.get("optimalWindowStart") or None, + optimal_window_end=harvest_card.get("optimalWindowEnd") or None, + chart_data=summary.get("yield_prediction_chart", {}), + )