From 21b734f6a7c498fff672bbda80ddd4094f141a2d Mon Sep 17 00:00:00 2001 From: Mohammad Sajad Pourajam Date: Sat, 2 May 2026 16:40:47 +0330 Subject: [PATCH] UPDATE --- celerybeat-schedule | Bin 16384 -> 16384 bytes docs/yield_harvest_prediction_api_changes.md | 32 ++++----- yield_harvest/serializers.py | 10 ++- yield_harvest/tests.py | 18 ++--- yield_harvest/views.py | 65 +++++++++---------- 5 files changed, 60 insertions(+), 65 deletions(-) diff --git a/celerybeat-schedule b/celerybeat-schedule index 8a6fc6ed83d35064f95c169ed299aad4d10b6e1d..449fca2f3461d42d743d443e3ae498336ac6f098 100644 GIT binary patch delta 27 icmZo@U~Fh$+|X{!#>>FKm~?saM}t5{=gs$x6?p)BvIy}2 delta 27 icmZo@U~Fh$+|X{!#?8RM7{@#Lqd_2}w2nZtp diff --git a/docs/yield_harvest_prediction_api_changes.md b/docs/yield_harvest_prediction_api_changes.md index fa33dd3..3adaa58 100644 --- a/docs/yield_harvest_prediction_api_changes.md +++ b/docs/yield_harvest_prediction_api_changes.md @@ -20,7 +20,7 @@ - `farm_uuid` ورودی اصلی و الزامی است. - `plant_name` اگر هم توسط client ارسال شود، مبنای نهایی backend نیست و از روی مزرعه بازنویسی/resolve می‌شود. -- در صورت نیاز، `irrigation_plan_id` و `fertilization_plan_id` هم می‌توانند ارسال شوند. +- در صورت نیاز، `irrigation_plan_uuid` و `fertilization_plan_uuid` هم می‌توانند ارسال شوند. - اگر plan انتخابی معتبر و متعلق به همان مزرعه کاربر باشد، backend محتوای آن را به payload ارسالی به AI اضافه می‌کند. - خروجی backend به‌صورت یکدست با فرمت `code / msg / data` برگردانده می‌شود. @@ -33,16 +33,16 @@ ```json { "farm_uuid": "11111111-1111-1111-1111-111111111111", - "irrigation_plan_id": 12, - "fertilization_plan_id": 34 + "irrigation_plan_uuid": "6d6a1f0d-1a9b-4f2f-8fe1-2d73d9d2d9f1", + "fertilization_plan_uuid": "7e7b2f1e-2b0c-4a3a-9fe2-3e84e0e3e0a2" } ``` ### فیلدها - `farm_uuid` اجباری -- `irrigation_plan_id` اختیاری -- `fertilization_plan_id` اختیاری +- `irrigation_plan_uuid` اختیاری +- `fertilization_plan_uuid` اختیاری ### نکته مهم @@ -56,8 +56,8 @@ - ورودی endpoint عملاً بر پایه `farm_uuid` کار می‌کند و `plant_name` از context مزرعه تعیین می‌شود. - backend به‌صورت خودکار `plant_name` را از مزرعه پیدا می‌کند. -- در صورت ارسال `irrigation_plan_id`، اطلاعات برنامه آبیاری داخل payload ارسالی به AI قرار می‌گیرد. -- در صورت ارسال `fertilization_plan_id`، اطلاعات برنامه کودی هم اضافه می‌شود. +- در صورت ارسال `irrigation_plan_uuid`، اطلاعات برنامه آبیاری داخل payload ارسالی به AI قرار می‌گیرد. +- در صورت ارسال `fertilization_plan_uuid`، اطلاعات برنامه کودی هم اضافه می‌شود. ### نمونه request @@ -122,7 +122,7 @@ ### تغییرات - ورودی endpoint عملاً بر پایه `farm_uuid` کار می‌کند و `plant_name` توسط backend تعیین می‌شود. -- امکان ارسال `fertilization_plan_id` و `irrigation_plan_id` برای enrich کردن context اضافه/پشتیبانی شده است. +- امکان ارسال `fertilization_plan_uuid` و `irrigation_plan_uuid` برای enrich کردن context اضافه/پشتیبانی شده است. - پاسخ AI بعد از extract شدن در `data.result`، به شکل مستقیم در `data` برگردانده می‌شود. ### نمونه request @@ -130,7 +130,7 @@ ```json { "farm_uuid": "11111111-1111-1111-1111-111111111111", - "fertilization_plan_id": 34 + "fertilization_plan_uuid": "7e7b2f1e-2b0c-4a3a-9fe2-3e84e0e3e0a2" } ``` @@ -176,7 +176,7 @@ - مثل دو endpoint دیگر، `plant_name` از روی مزرعه resolve می‌شود. - در نبود محصول مستقیم روی مزرعه، backend از fallback مناسب مزرعه استفاده می‌کند. -- امکان ارسال `irrigation_plan_id` و `fertilization_plan_id` برای فرستادن context planها به AI اضافه/پشتیبانی شده است. +- امکان ارسال `irrigation_plan_uuid` و `fertilization_plan_uuid` برای فرستادن context planها به AI اضافه/پشتیبانی شده است. - پاسخ نهایی با ساختار یکنواخت `code / msg / data` برگردانده می‌شود. ### نمونه request @@ -184,8 +184,8 @@ ```json { "farm_uuid": "11111111-1111-1111-1111-111111111111", - "irrigation_plan_id": 12, - "fertilization_plan_id": 34 + "irrigation_plan_uuid": "6d6a1f0d-1a9b-4f2f-8fe1-2d73d9d2d9f1", + "fertilization_plan_uuid": "7e7b2f1e-2b0c-4a3a-9fe2-3e84e0e3e0a2" } ``` @@ -238,7 +238,7 @@ ### 2) plan نامعتبر یا متعلق به مزرعه دیگر -اگر `irrigation_plan_id` یا `fertilization_plan_id` متعلق به همان مزرعه کاربر نباشد، درخواست با خطا رد می‌شود. +اگر `irrigation_plan_uuid` یا `fertilization_plan_uuid` متعلق به همان مزرعه کاربر نباشد، درخواست با خطا رد می‌شود. نمونه: @@ -247,7 +247,7 @@ "code": 404, "msg": "error", "data": { - "irrigation_plan_id": [ + "irrigation_plan_uuid": [ "Irrigation plan not found." ] } @@ -256,7 +256,7 @@ ### 3) خطای validation ورودی -اگر `farm_uuid` ارسال نشود یا `plan_id`ها نامعتبر باشند، serializer خطای validation برمی‌گرداند. +اگر `farm_uuid` ارسال نشود یا `plan_uuid`ها نامعتبر باشند، serializer خطای validation برمی‌گرداند. --- @@ -264,6 +264,6 @@ - دیگر لازم نیست `plant_name` را برای این 3 API بفرستید. - فقط `farm_uuid` اجباری است. -- اگر کاربر plan خاصی را انتخاب کرده، `irrigation_plan_id` و/یا `fertilization_plan_id` را هم بفرستید. +- اگر کاربر plan خاصی را انتخاب کرده، `irrigation_plan_uuid` و/یا `fertilization_plan_uuid` را هم بفرستید. - response هر 3 endpoint با ساختار یکنواخت `code`, `msg`, `data` برمی‌گردد. - backend خودش payload مناسب AI را از context مزرعه و planهای انتخابی می‌سازد. diff --git a/yield_harvest/serializers.py b/yield_harvest/serializers.py index 784972e..428ccd4 100644 --- a/yield_harvest/serializers.py +++ b/yield_harvest/serializers.py @@ -64,15 +64,13 @@ class CropSimulationRequestSerializer(serializers.Serializer): initial="11111111-1111-1111-1111-111111111111", help_text="UUID مزرعه برای اجرای شبیه‌سازی.", ) - irrigation_plan_id = serializers.IntegerField( + irrigation_plan_uuid = serializers.UUIDField( required=False, - min_value=1, - help_text="شناسه داخلی برنامه آبیاری برای ارسال context به AI.", + help_text="UUID برنامه آبیاری برای ارسال context به AI.", ) - fertilization_plan_id = serializers.IntegerField( + fertilization_plan_uuid = serializers.UUIDField( required=False, - min_value=1, - help_text="شناسه داخلی برنامه کودی برای ارسال context به AI.", + help_text="UUID برنامه کودی برای ارسال context به AI.", ) diff --git a/yield_harvest/tests.py b/yield_harvest/tests.py index 3cc21e4..e0e3f83 100644 --- a/yield_harvest/tests.py +++ b/yield_harvest/tests.py @@ -395,13 +395,14 @@ class CropSimulationViewTests(TestCase): response = self.api_client.post( "/api/yield-harvest/current-farm-chart/", - {"farm_uuid": str(self.farm.farm_uuid), "irrigation_plan_id": irrigation_plan.id}, + {"farm_uuid": str(self.farm.farm_uuid), "irrigation_plan_uuid": str(irrigation_plan.uuid)}, format="json", ) self.assertEqual(response.status_code, 200) sent_payload = mock_external_api_request.call_args.kwargs["payload"] self.assertEqual(sent_payload["irrigation_plan"]["id"], irrigation_plan.id) + self.assertEqual(sent_payload["irrigation_plan"]["uuid"], str(irrigation_plan.uuid)) @patch("yield_harvest.views.external_api_request") def test_harvest_prediction_includes_selected_plans(self, mock_external_api_request): @@ -418,13 +419,14 @@ class CropSimulationViewTests(TestCase): response = self.api_client.post( "/api/yield-harvest/harvest-prediction/", - {"farm_uuid": str(self.farm.farm_uuid), "fertilization_plan_id": fertilization_plan.id}, + {"farm_uuid": str(self.farm.farm_uuid), "fertilization_plan_uuid": str(fertilization_plan.uuid)}, format="json", ) self.assertEqual(response.status_code, 200) sent_payload = mock_external_api_request.call_args.kwargs["payload"] self.assertEqual(sent_payload["fertilization_plan"]["id"], fertilization_plan.id) + self.assertEqual(sent_payload["fertilization_plan"]["uuid"], str(fertilization_plan.uuid)) @patch("yield_harvest.views.external_api_request") def test_yield_prediction_proxies_to_ai_service(self, mock_external_api_request): @@ -549,8 +551,8 @@ class CropSimulationViewTests(TestCase): "/api/yield-harvest/yield-prediction/", { "farm_uuid": str(self.farm.farm_uuid), - "irrigation_plan_id": irrigation_plan.id, - "fertilization_plan_id": fertilization_plan.id, + "irrigation_plan_uuid": str(irrigation_plan.uuid), + "fertilization_plan_uuid": str(fertilization_plan.uuid), }, format="json", ) @@ -565,7 +567,7 @@ class CropSimulationViewTests(TestCase): "npk-202020", ) - def test_yield_prediction_rejects_foreign_plan_ids(self): + def test_yield_prediction_rejects_foreign_plan_uuids(self): other_irrigation_plan = IrrigationPlan.objects.create( farm=self.other_farm, source=IrrigationPlan.SOURCE_FREE_TEXT, @@ -576,13 +578,13 @@ class CropSimulationViewTests(TestCase): "/api/yield-harvest/yield-prediction/", { "farm_uuid": str(self.farm.farm_uuid), - "irrigation_plan_id": other_irrigation_plan.id, + "irrigation_plan_uuid": str(other_irrigation_plan.uuid), }, format="json", ) self.assertEqual(response.status_code, 404) - self.assertEqual(response.json()["data"]["irrigation_plan_id"][0], "Irrigation plan not found.") + self.assertEqual(response.json()["data"]["irrigation_plan_uuid"][0], "Irrigation plan not found.") @patch("yield_harvest.views.external_api_request") def test_yield_harvest_summary_top_level_route_proxies_to_ai_service(self, mock_external_api_request): @@ -654,7 +656,7 @@ class CropSimulationViewTests(TestCase): ) response = self.api_client.get( - f"/api/yield-harvest/yield-harvest-summary/?farm_uuid={self.farm.farm_uuid}&irrigation_plan_id={irrigation_plan.id}&fertilization_plan_id={fertilization_plan.id}" + f"/api/yield-harvest/yield-harvest-summary/?farm_uuid={self.farm.farm_uuid}&irrigation_plan_uuid={irrigation_plan.uuid}&fertilization_plan_uuid={fertilization_plan.uuid}" ) self.assertEqual(response.status_code, 200) diff --git a/yield_harvest/views.py b/yield_harvest/views.py index a588c6e..0883c68 100644 --- a/yield_harvest/views.py +++ b/yield_harvest/views.py @@ -79,16 +79,16 @@ class YieldHarvestSummaryView(APIView): if error_response is not None: return error_response - irrigation_plan_id, irrigation_plan_error = CropSimulationBaseView._parse_optional_plan_id( - request.query_params.get("irrigation_plan_id"), - "irrigation_plan_id", + irrigation_plan_uuid, irrigation_plan_error = CropSimulationBaseView._parse_optional_plan_uuid( + request.query_params.get("irrigation_plan_uuid"), + "irrigation_plan_uuid", ) if irrigation_plan_error is not None: return irrigation_plan_error - fertilization_plan_id, fertilization_plan_error = CropSimulationBaseView._parse_optional_plan_id( - request.query_params.get("fertilization_plan_id"), - "fertilization_plan_id", + fertilization_plan_uuid, fertilization_plan_error = CropSimulationBaseView._parse_optional_plan_uuid( + request.query_params.get("fertilization_plan_uuid"), + "fertilization_plan_uuid", ) if fertilization_plan_error is not None: return fertilization_plan_error @@ -101,10 +101,10 @@ class YieldHarvestSummaryView(APIView): if request.query_params.get("include_narrative") is not None: query["include_narrative"] = request.query_params.get("include_narrative") - ai_payload, plan_error = self._build_ai_payload_with_selected_plans( + ai_payload, plan_error = CropSimulationBaseView()._build_ai_payload_with_selected_plans( farm, - irrigation_plan_id=irrigation_plan_id, - fertilization_plan_id=fertilization_plan_id, + irrigation_plan_uuid=irrigation_plan_uuid, + fertilization_plan_uuid=fertilization_plan_uuid, ) if plan_error is not None: return plan_error @@ -217,35 +217,35 @@ class CropSimulationBaseView(APIView): return "" @staticmethod - def _get_irrigation_plan_or_error(farm, plan_id): - if not plan_id: + def _get_irrigation_plan_or_error(farm, plan_uuid): + if not plan_uuid: return None, None plan = IrrigationPlan.objects.filter( - id=plan_id, + uuid=plan_uuid, farm=farm, is_deleted=False, ).first() if plan is None: return None, Response( - {"code": 404, "msg": "error", "data": {"irrigation_plan_id": ["Irrigation plan not found."]}}, + {"code": 404, "msg": "error", "data": {"irrigation_plan_uuid": ["Irrigation plan not found."]}}, status=status.HTTP_404_NOT_FOUND, ) return plan, None @staticmethod - def _get_fertilization_plan_or_error(farm, plan_id): - if not plan_id: + def _get_fertilization_plan_or_error(farm, plan_uuid): + if not plan_uuid: return None, None plan = FertilizationPlan.objects.filter( - id=plan_id, + uuid=plan_uuid, farm=farm, is_deleted=False, ).first() if plan is None: return None, Response( - {"code": 404, "msg": "error", "data": {"fertilization_plan_id": ["Fertilization plan not found."]}}, + {"code": 404, "msg": "error", "data": {"fertilization_plan_uuid": ["Fertilization plan not found."]}}, status=status.HTTP_404_NOT_FOUND, ) return plan, None @@ -268,13 +268,13 @@ class CropSimulationBaseView(APIView): "response_payload": plan.response_payload if isinstance(plan.response_payload, dict) else {}, } - def _build_ai_payload_with_selected_plans(self, farm, irrigation_plan_id=None, fertilization_plan_id=None): - irrigation_plan, irrigation_error = self._get_irrigation_plan_or_error(farm, irrigation_plan_id) + def _build_ai_payload_with_selected_plans(self, farm, irrigation_plan_uuid=None, fertilization_plan_uuid=None): + irrigation_plan, irrigation_error = self._get_irrigation_plan_or_error(farm, irrigation_plan_uuid) if irrigation_error is not None: return None, irrigation_error fertilization_plan, fertilization_error = self._get_fertilization_plan_or_error( - farm, fertilization_plan_id + farm, fertilization_plan_uuid ) if fertilization_error is not None: return None, fertilization_error @@ -291,19 +291,14 @@ class CropSimulationBaseView(APIView): return ai_payload, None @staticmethod - def _parse_optional_plan_id(raw_value, field_name): + def _parse_optional_plan_uuid(raw_value, field_name): if raw_value in (None, ""): return None, None try: - parsed_value = int(raw_value) - except (TypeError, ValueError): + parsed_value = str(serializers.UUIDField().to_internal_value(raw_value)) + except serializers.ValidationError: return None, Response( - {"code": 400, "msg": "error", "data": {field_name: ["A valid integer is required."]}}, - status=status.HTTP_400_BAD_REQUEST, - ) - if parsed_value < 1: - return None, Response( - {"code": 400, "msg": "error", "data": {field_name: ["Ensure this value is greater than or equal to 1."]}}, + {"code": 400, "msg": "error", "data": {field_name: ["Must be a valid UUID."]}}, status=status.HTTP_400_BAD_REQUEST, ) return parsed_value, None @@ -328,8 +323,8 @@ class CurrentFarmChartView(CropSimulationBaseView): ai_payload, plan_error = self._build_ai_payload_with_selected_plans( farm, - irrigation_plan_id=payload.get("irrigation_plan_id"), - fertilization_plan_id=payload.get("fertilization_plan_id"), + irrigation_plan_uuid=payload.get("irrigation_plan_uuid"), + fertilization_plan_uuid=payload.get("fertilization_plan_uuid"), ) if plan_error is not None: return plan_error @@ -363,8 +358,8 @@ class HarvestPredictionView(CropSimulationBaseView): ai_payload, plan_error = self._build_ai_payload_with_selected_plans( farm, - irrigation_plan_id=payload.get("irrigation_plan_id"), - fertilization_plan_id=payload.get("fertilization_plan_id"), + irrigation_plan_uuid=payload.get("irrigation_plan_uuid"), + fertilization_plan_uuid=payload.get("fertilization_plan_uuid"), ) if plan_error is not None: return plan_error @@ -398,8 +393,8 @@ class YieldPredictionView(CropSimulationBaseView): ai_payload, plan_error = self._build_ai_payload_with_selected_plans( farm, - irrigation_plan_id=payload.get("irrigation_plan_id"), - fertilization_plan_id=payload.get("fertilization_plan_id"), + irrigation_plan_uuid=payload.get("irrigation_plan_uuid"), + fertilization_plan_uuid=payload.get("fertilization_plan_uuid"), ) if plan_error is not None: return plan_error