diff --git a/celerybeat-schedule b/celerybeat-schedule index fdd0c09..7e2dc2c 100644 Binary files a/celerybeat-schedule and b/celerybeat-schedule differ diff --git a/config/urls.py b/config/urls.py index 2ea3f7e..0ed4d0b 100644 --- a/config/urls.py +++ b/config/urls.py @@ -18,8 +18,8 @@ urlpatterns = [ path("api/soil/", include("soil.urls")), path("api/crop-zoning/", include("crop_zoning.urls")), - path("api/yield-harvest/", include("yield_harvest.urls")), - path("api/crop-simulation/", include("yield_harvest.crop_simulation_urls")), + # path("api/yield-harvest/", include("yield_harvest.urls")), + path("api/yield-harvest/", include("yield_harvest.crop_simulation_urls")), path("api/pest-detection/", include("pest_detection.urls")), path("api/pest-disease/", include("pest_detection.pest_disease_urls")), diff --git a/docs/yield_harvest_ai_integration.md b/docs/yield_harvest_ai_integration.md new file mode 100644 index 0000000..544e809 --- /dev/null +++ b/docs/yield_harvest_ai_integration.md @@ -0,0 +1,941 @@ +# مرجع کامل ارتباط Backend با AI در ماژول Yield & Harvest + +این سند قرارداد فعلی backend برای endpointهای ماژول `yield_harvest` را توضیح می‌دهد؛ هم از دید فرانت/کاربر، هم از دید payload ارسالی به سرویس AI. + +این سند این endpointها را پوشش می‌دهد: + +- `POST /api/yield-harvest/current-farm-chart/` +- `POST /api/yield-harvest/growth/` +- `GET /api/yield-harvest/growth/{task_id}/status/` +- `POST /api/yield-harvest/harvest-prediction/` +- `POST /api/yield-harvest/yield-prediction/` +- `GET /api/yield-harvest/yield-harvest-summary/` + +--- + +## هدف این سند + +این ماژول باید برای endpointهای farm-based تا حد ممکن فقط `farm_uuid` را از کاربر بگیرد و بقیه context لازم را خودش از دیتابیس بخواند. + +مهم‌ترین قاعده این سند: + +- فرانت نباید `plant_name` را برای endpointهای farm-based ارسال کند. +- backend باید `plant_name` را از `farm_hub.models.FarmHub` استخراج کند. +- منبع استخراج `plant_name` این است: + 1. اولین محصول `farm.products` بر اساس `id` + 2. اگر مزرعه محصول نداشت، اولین محصول `farm.farm_type.products` بر اساس `id` + +پیاده‌سازی فعلی این رفتار در فایل زیر است: + +- `yield_harvest/views.py` + +مدل‌های منبع داده: + +- `farm_hub/models.py` + +--- + +## احراز هویت و سطح دسترسی + +همه endpointهای این سند نیاز به JWT معتبر دارند. + +### هدرهای متداول + +```http +Authorization: Bearer +Content-Type: application/json +Accept: application/json +``` + +### اعتبارسنجی مالکیت مزرعه + +برای endpointهایی که `farm_uuid` می‌گیرند، backend فقط زمانی درخواست را قبول می‌کند که: + +- مزرعه وجود داشته باشد +- و مالک آن مزرعه همان `request.user` باشد + +اگر مزرعه برای کاربر جاری پیدا نشود: + +```json +{ + "code": 404, + "msg": "error", + "data": { + "farm_uuid": ["Farm not found."] + } +} +``` + +--- + +## الگوی کلی پاسخ‌ها + +تقریباً تمام endpointهای این ماژول از envelope زیر استفاده می‌کنند: + +```json +{ + "code": 200, + "msg": "success", + "data": {} +} +``` + +### معنی فیلدهای envelope + +| فیلد | نوع | توضیح | +|---|---|---| +| `code` | integer | کد منطقی پاسخ؛ معمولاً با HTTP status هم‌راستا است | +| `msg` | string | پیام کوتاه پاسخ | +| `data` | object / array / null | بدنه اصلی پاسخ | + +### خطاهای متداول + +| HTTP Status | `code` | توضیح | +|---|---|---| +| `400` | `400` | ورودی نامعتبر است | +| `404` | `404` | مزرعه برای کاربر جاری پیدا نشد | +| `500` | `500` | AI یا لایه محاسباتی upstream خطا داده است | +| `202` | `202` | تسک async با موفقیت در صف قرار گرفته است | + +--- + +## قرارداد ورودی از دید Frontend + +### اصل طراحی + +برای endpointهای farm-based این ماژول، فرانت فقط باید `farm_uuid` را ارسال کند و نباید موارد زیر را از کاربر بگیرد: + +- `plant_name` +- `crop_name` برای جریان‌های farm-based prediction +- هر context دیگری که backend می‌تواند از مزرعه استخراج کند + +### استثناها + +- `GET /api/yield-harvest/growth/{task_id}/status/` به `farm_uuid` نیاز ندارد؛ چون بر اساس `task_id` کار می‌کند. +- `GET /api/yield-harvest/yield-harvest-summary/` علاوه بر `farm_uuid` می‌تواند queryهای اختیاری هم داشته باشد، ولی در قرارداد فرانت ساده می‌توان فقط `farm_uuid` را فرستاد. +- endpoint رشد (`growth`) در لایه AI پارامترهای پیشرفته دارد، اما در قرارداد ساده frontend این سند، ورودی کاربر باید فقط `farm_uuid` باشد و backend باید context گیاه را از مزرعه بردارد. + +--- + +## نگاشت endpointهای Backend به AI + +| Backend Route | Method | AI Route | Method | +|---|---|---|---| +| `/api/yield-harvest/current-farm-chart/` | `POST` | `/api/crop-simulation/current-farm-chart/` | `POST` | +| `/api/yield-harvest/growth/` | `POST` | `/api/crop-simulation/growth/` | `POST` | +| `/api/yield-harvest/growth/{task_id}/status/` | `GET` | `/api/crop-simulation/growth/{task_id}/status/` | `GET` | +| `/api/yield-harvest/harvest-prediction/` | `POST` | `/api/crop-simulation/harvest-prediction/` | `POST` | +| `/api/yield-harvest/yield-prediction/` | `POST` | `/api/crop-simulation/yield-prediction/` | `POST` | +| `/api/yield-harvest/yield-harvest-summary/` | `GET` | `/api/crop-simulation/yield-harvest-summary/` | `GET` | + +--- + +## منبع `plant_name` در Backend + +### منبع داده + +backend نام گیاه را از مدل `FarmHub` در `farm_hub/models.py` می‌گیرد. + +### ترتیب انتخاب + +1. `farm.products.order_by("id").first()` +2. اگر مورد 1 خالی بود: `farm.farm_type.products.order_by("id").first()` + +### مثال مفهومی + +اگر مزرعه این محصولات را داشته باشد: + +```text +farm.products = ["خیار", "گوجه‌فرنگی"] +``` + +backend این مقدار را برای AI می‌فرستد: + +```json +{ + "farm_uuid": "11111111-1111-1111-1111-111111111111", + "plant_name": "خیار" +} +``` + +یعنی معیار فعلی، «اولین محصول بر اساس `id`» است، نه محصول انتخاب‌شده توسط کاربر. + +--- + +## 1) POST `/api/yield-harvest/current-farm-chart/` + +### کاربرد + +دریافت نمودار وضعیت فعلی مزرعه بر اساس شبیه‌سازی رشد محصول. + +### ورودی از فرانت + +```json +{ + "farm_uuid": "11111111-1111-1111-1111-111111111111" +} +``` + +### نکته مهم + +- `plant_name` از کاربر گرفته نمی‌شود. +- backend آن را از مزرعه استخراج می‌کند. + +### payload ارسالی backend به AI + +نمونه: + +```json +{ + "farm_uuid": "11111111-1111-1111-1111-111111111111", + "plant_name": "خیار" +} +``` + +### پاسخ موفق + +```json +{ + "code": 200, + "msg": "success", + "data": { + "farm_uuid": "11111111-1111-1111-1111-111111111111", + "plant_name": "گوجه‌فرنگی", + "engine": "growth_projection", + "model_name": "growth_projection_v1", + "scenario_id": 12, + "simulation_warning": null, + "categories": ["2026-04-01", "2026-04-02"], + "series": [ + { + "name": "تعداد برگ تخمینی", + "key": "leaf_count_estimate", + "data": [120.0, 140.0] + } + ], + "summary": [ + { + "title": "تعداد برگ تخمینی", + "subtitle": "وضعیت فعلی", + "amount": 140.0, + "unit": "leaf", + "avatarColor": "success", + "avatarIcon": "tabler-leaf" + } + ], + "current_state": { + "date": "2026-04-02", + "leaf_count_estimate": 140.0, + "leaf_area_index": 0.0117, + "biomass_weight": 45.0, + "storage_organ_weight": 10.0, + "soil_moisture_percent": 41.2, + "development_stage": 0.35, + "gdd": 9.0 + }, + "metrics": { + "yield_estimate": 10.0 + }, + "daily_output": [] + } +} +``` + +### توضیح فیلدهای پاسخ + +| فیلد | نوع | توضیح | +|---|---|---| +| `farm_uuid` | string / null | شناسه مزرعه | +| `plant_name` | string | نام گیاهی که شبیه‌سازی برای آن انجام شده | +| `engine` | string / null | موتور شبیه‌سازی | +| `model_name` | string / null | نام مدل | +| `scenario_id` | integer / null | شناسه سناریو | +| `simulation_warning` | string / null | هشدار غیر بحرانی | +| `categories` | array[string] | محور زمانی نمودار | +| `series` | array[object] | سری‌های نمودار | +| `summary` | array[object] | کارت‌های خلاصه | +| `current_state` | object | وضعیت آخرین روز شبیه‌سازی | +| `metrics` | object | شاخص‌های محاسبه‌شده | +| `daily_output` | array[object] | خروجی خام روزانه | + +### توضیح `series[]` + +| فیلد | نوع | توضیح | +|---|---|---| +| `name` | string | عنوان سری | +| `key` | string | کلید فنی سری | +| `data` | array[number] | مقادیر سری | + +### توضیح `summary[]` + +| فیلد | نوع | توضیح | +|---|---|---| +| `title` | string | عنوان کارت | +| `subtitle` | string | زیرعنوان | +| `amount` | number | مقدار اصلی | +| `unit` | string | واحد | +| `avatarColor` | string | رنگ پیشنهادی UI | +| `avatarIcon` | string | آیکن پیشنهادی UI | + +### توضیح `current_state` + +| فیلد | نوع | توضیح | +|---|---|---| +| `date` | string | تاریخ آخرین رکورد | +| `leaf_count_estimate` | number | تعداد برگ تخمینی | +| `leaf_area_index` | number | شاخص سطح برگ | +| `biomass_weight` | number | وزن بیوماس | +| `storage_organ_weight` | number | وزن اندام ذخیره‌ای / محصول | +| `soil_moisture_percent` | number | درصد رطوبت خاک | +| `development_stage` | number | مرحله رشد | +| `gdd` | number | درجه-روز رشد | + +--- + +## 2) POST `/api/yield-harvest/growth/` + +### کاربرد + +شروع شبیه‌سازی رشد به صورت async. + +### قرارداد ساده فرانت + +در قرارداد frontend این سند، فرانت فقط باید `farm_uuid` را بفرستد. + +```json +{ + "farm_uuid": "11111111-1111-1111-1111-111111111111" +} +``` + +### نکته مهم + +- `plant_name` نباید از کاربر گرفته شود. +- backend آن را از مزرعه استخراج می‌کند. +- `task_id` خروجی این endpoint، ورودی endpoint وضعیت است. + +### payload ارسالی backend به AI + +نمونه مفهومی: + +```json +{ + "farm_uuid": "11111111-1111-1111-1111-111111111111", + "plant_name": "خیار", + "dynamic_parameters": ["DVS", "LAI", "TAGP", "TWSO", "SM"] +} +``` + +نکته: upstream AI ممکن است پارامترهای پیشرفته بیشتری هم بپذیرد، ولی این‌ها نباید از کاربر نهایی گرفته شوند مگر این‌که قرارداد جداگانه‌ای برای expert mode تعریف شود. + +### پاسخ موفق + +```json +{ + "code": 202, + "msg": "تسک شبیه سازی رشد در صف قرار گرفت.", + "data": { + "task_id": "growth-task-1", + "status_url": "/api/crop-simulation/growth/growth-task-1/status/", + "plant_name": "گوجه‌فرنگی" + } +} +``` + +### توضیح فیلدهای پاسخ + +| فیلد | نوع | توضیح | +|---|---|---| +| `task_id` | string | شناسه تسک | +| `status_url` | string | آدرس بررسی وضعیت تسک | +| `plant_name` | string | نام گیاهی که شبیه‌سازی برای آن آغاز شده | + +--- + +## 3) GET `/api/yield-harvest/growth/{task_id}/status/` + +### کاربرد + +بررسی وضعیت و نتیجه تسک async شبیه‌سازی رشد. + +### ورودی + +این endpoint از کاربر `farm_uuid` نمی‌گیرد. + +### Path Parameter + +| فیلد | نوع | اجباری | توضیح | +|---|---|---:|---| +| `task_id` | string | بله | شناسه تسک برگشتی از endpoint رشد | + +### Query اختیاری + +| فیلد | نوع | اجباری | توضیح | +|---|---|---:|---| +| `page` | integer | خیر | شماره صفحه stageها | +| `page_size` | integer | خیر | تعداد آیتم در هر صفحه | + +### پاسخ در حالت `PENDING` + +```json +{ + "code": 200, + "msg": "success", + "data": { + "task_id": "growth-task-1", + "status": "PENDING", + "message": "تسک در صف یا یافت نشد." + } +} +``` + +### پاسخ در حالت `PROGRESS` + +```json +{ + "code": 200, + "msg": "success", + "data": { + "task_id": "growth-task-1", + "status": "PROGRESS", + "progress": { + "current": 2, + "total": 3, + "percent": 66.7 + } + } +} +``` + +### پاسخ در حالت `SUCCESS` + +```json +{ + "code": 200, + "msg": "success", + "data": { + "task_id": "growth-task-1", + "status": "SUCCESS", + "result": { + "plant_name": "گوجه‌فرنگی", + "dynamic_parameters": ["DVS"], + "engine": "growth_projection", + "model_name": "growth_projection_v1", + "scenario_id": null, + "simulation_warning": null, + "summary_metrics": {}, + "stage_timeline": [], + "stages_page": [], + "pagination": { + "page": 1, + "page_size": 10, + "total_items": 0, + "total_pages": 0, + "has_next": false, + "has_previous": false + }, + "daily_records_count": 0, + "default_page_size": 10 + } + } +} +``` + +### پاسخ در حالت `FAILURE` + +```json +{ + "code": 200, + "msg": "success", + "data": { + "task_id": "growth-task-1", + "status": "FAILURE", + "error": "task crashed" + } +} +``` + +### توضیح فیلدهای status response + +| فیلد | نوع | توضیح | +|---|---|---| +| `task_id` | string | شناسه تسک | +| `status` | string | وضعیت تسک: `PENDING`, `PROGRESS`, `SUCCESS`, `FAILURE` | +| `message` | string | پیام کمکی در برخی وضعیت‌ها | +| `progress` | object | وضعیت پیشرفت | +| `result` | object | نتیجه نهایی در حالت موفق | +| `error` | string | خطای نهایی در حالت failure | + +### توضیح `progress` + +| فیلد | نوع | توضیح | +|---|---|---| +| `current` | integer | مرحله فعلی | +| `total` | integer | کل مراحل | +| `percent` | float | درصد پیشرفت | + +### توضیح `result` + +| فیلد | نوع | توضیح | +|---|---|---| +| `plant_name` | string | نام گیاه | +| `dynamic_parameters` | array[string] | پارامترهای دینامیک | +| `engine` | string / null | موتور شبیه‌سازی | +| `model_name` | string / null | نام مدل | +| `scenario_id` | integer / null | شناسه سناریو | +| `simulation_warning` | string / null | هشدار محاسباتی | +| `summary_metrics` | object | شاخص‌های خلاصه | +| `stage_timeline` | array[object] | timeline کامل مراحل | +| `stages_page` | array[object] | آیتم‌های همین صفحه | +| `pagination` | object | اطلاعات صفحه‌بندی | +| `daily_records_count` | integer | تعداد رکوردهای روزانه | +| `default_page_size` | integer | اندازه صفحه پیش‌فرض | + +### توضیح `pagination` + +| فیلد | نوع | توضیح | +|---|---|---| +| `page` | integer | صفحه فعلی | +| `page_size` | integer | اندازه صفحه | +| `total_items` | integer | تعداد کل stageها | +| `total_pages` | integer | تعداد کل صفحه‌ها | +| `has_next` | boolean | آیا صفحه بعدی وجود دارد | +| `has_previous` | boolean | آیا صفحه قبلی وجود دارد | + +--- + +## 4) POST `/api/yield-harvest/harvest-prediction/` + +### کاربرد + +پیش‌بینی زمان برداشت برای مزرعه. + +### ورودی از فرانت + +```json +{ + "farm_uuid": "11111111-1111-1111-1111-111111111111" +} +``` + +### payload ارسالی backend به AI + +```json +{ + "farm_uuid": "11111111-1111-1111-1111-111111111111", + "plant_name": "خیار" +} +``` + +### پاسخ موفق + +```json +{ + "code": 200, + "msg": "success", + "data": { + "date": "2026-05-14", + "dateFormatted": "14 May 2026", + "daysUntil": 43, + "description": "شبيه ساز نشان مي دهد حدود 43 روز ديگر تا برداشت باقي مانده است.", + "optimalWindowStart": "2026-05-11", + "optimalWindowEnd": "2026-05-17", + "gddDetails": { + "current_cumulative_gdd": 50.0, + "required_gdd_for_maturity": 1200.0, + "remaining_gdd": 1150.0, + "simulation_engine": "growth_projection" + } + } +} +``` + +### توضیح فیلدهای پاسخ + +| فیلد | نوع | توضیح | +|---|---|---| +| `date` | string | تاریخ تخمینی برداشت به فرمت ISO | +| `dateFormatted` | string | تاریخ قابل نمایش | +| `daysUntil` | integer | تعداد روزهای باقیمانده | +| `description` | string | توضیح متنی | +| `optimalWindowStart` | string | شروع پنجره مناسب برداشت | +| `optimalWindowEnd` | string | پایان پنجره مناسب برداشت | +| `gddDetails` | object | جزئیات محاسبات GDD | + +### توضیح `gddDetails` + +| فیلد | نوع | توضیح | +|---|---|---| +| `current_cumulative_gdd` | number | GDD تجمعی فعلی | +| `required_gdd_for_maturity` | number | GDD مورد نیاز برای بلوغ | +| `remaining_gdd` | number | GDD باقی‌مانده | +| `estimated_days_to_harvest` | integer | روزهای برآوردی تا برداشت | +| `predicted_harvest_date` | string | تاریخ برآوردی برداشت | +| `predicted_harvest_window` | object | بازه برداشت | +| `daily_gdd_forecast` | array[object] | پیش‌بینی روزانه GDD | +| `simulation_engine` | string | موتور شبیه‌سازی | +| `simulation_model_name` | string | نام مدل | +| `simulation_warning` | string / null | هشدار محاسباتی | +| `scenario_id` | integer / null | شناسه سناریو | + +--- + +## 5) POST `/api/yield-harvest/yield-prediction/` + +### کاربرد + +پیش‌بینی عملکرد مزرعه. + +### ورودی از فرانت + +```json +{ + "farm_uuid": "11111111-1111-1111-1111-111111111111" +} +``` + +### payload ارسالی backend به AI + +```json +{ + "farm_uuid": "11111111-1111-1111-1111-111111111111", + "plant_name": "خیار" +} +``` + +### پاسخ موفق + +```json +{ + "code": 200, + "msg": "success", + "data": { + "farm_uuid": "11111111-1111-1111-1111-111111111111", + "plant_name": "گوجه‌فرنگی", + "predictedYieldTons": 5.4, + "predictedYieldRaw": 5400.0, + "unit": "تن", + "sourceUnit": "kg/ha", + "simulationEngine": "growth_projection", + "simulationModel": "growth_projection_v1", + "scenarioId": 12, + "simulationWarning": null, + "supportingMetrics": { + "yield_estimate": 5400.0 + } + } +} +``` + +### توضیح فیلدهای پاسخ + +| فیلد | نوع | توضیح | +|---|---|---| +| `farm_uuid` | string | شناسه مزرعه | +| `plant_name` | string / null | نام گیاه | +| `predictedYieldTons` | number | عملکرد بر حسب تن | +| `predictedYieldRaw` | number | مقدار خام عملکرد | +| `unit` | string | واحد نمایشی | +| `sourceUnit` | string | واحد منبع | +| `simulationEngine` | string / null | موتور شبیه‌سازی | +| `simulationModel` | string / null | نام مدل شبیه‌سازی | +| `scenarioId` | integer / null | شناسه سناریو | +| `simulationWarning` | string / null | هشدار محاسباتی | +| `supportingMetrics` | object | شاخص‌های پشتیبان | + +### توضیح `supportingMetrics` + +این object بسته به upstream می‌تواند شامل مواردی مانند این‌ها باشد: + +| فیلد | نوع | توضیح | +|---|---|---| +| `yield_estimate` | number | برآورد خام عملکرد | +| `biomass` | number | بیوماس برآوردی | +| `max_lai` | number | بیشترین شاخص سطح برگ | + +--- + +## 6) GET `/api/yield-harvest/yield-harvest-summary/` + +### کاربرد + +دریافت داشبورد کامل عملکرد و برداشت. + +### ورودی ساده از فرانت + +```http +GET /api/yield-harvest/yield-harvest-summary/?farm_uuid=11111111-1111-1111-1111-111111111111 +``` + +### Queryهای اختیاری قابل پشتیبانی + +| فیلد | نوع | اجباری | توضیح | +|---|---|---:|---| +| `farm_uuid` | UUID | بله | شناسه مزرعه | +| `season_year` | integer | خیر | سال زراعی | +| `crop_name` | string | خیر | نام محصول | +| `include_narrative` | boolean | خیر | در صورت `true` متن‌های توضیحی هم merge می‌شوند | + +### نکته قرارداد فرانت + +در جریان ساده frontend، ارسال فقط `farm_uuid` کافی است و backend بقیه context لازم را مدیریت می‌کند. + +### پاسخ موفق + +```json +{ + "code": 200, + "msg": "success", + "data": { + "farm_uuid": "11111111-1111-1111-1111-111111111111", + "season_highlights_card": {}, + "yield_prediction": {}, + "harvest_prediction_card": {}, + "harvest_readiness_zones": {}, + "yield_quality_bands": {}, + "harvest_operations_card": {}, + "yield_prediction_chart": {} + } +} +``` + +### توضیح top-level response + +| فیلد | نوع | توضیح | +|---|---|---| +| `farm_uuid` | string | شناسه مزرعه | +| `season_highlights_card` | object | خلاصه مهم‌ترین KPIها | +| `yield_prediction` | object | خروجی پیش‌بینی عملکرد | +| `harvest_prediction_card` | object | تاریخ و وضعیت برداشت | +| `harvest_readiness_zones` | object | آمادگی برداشت در zoneها | +| `yield_quality_bands` | object | کیفیت برآوردی محصول | +| `harvest_operations_card` | object | عملیات پیشنهادی برداشت | +| `yield_prediction_chart` | object | نمودار عملکرد و بیوماس | + +### توضیح `season_highlights_card` + +| فیلد | نوع | توضیح | +|---|---|---| +| `title` | string | عنوان کارت | +| `subtitle` | string | توضیح کوتاه | +| `total_predicted_yield` | number / null | عملکرد پیش‌بینی‌شده | +| `yield_unit` | string | واحد عملکرد | +| `target_harvest_date` | string / null | تاریخ هدف برداشت | +| `days_until_harvest` | integer / null | روز باقی‌مانده | +| `average_readiness` | number / null | میانگین آمادگی | +| `primary_quality_grade` | string / null | گرید کیفیت غالب | +| `estimated_revenue` | number / null | درآمد تخمینی | +| `soil_type` | string / null | نوع خاک | + +### توضیح `yield_prediction` + +| فیلد | نوع | توضیح | +|---|---|---| +| `predicted_yield_tons` | number | عملکرد بر حسب تن | +| `predicted_yield_raw` | number | عملکرد خام | +| `unit` | string | واحد نمایشی | +| `source_unit` | string | واحد منبع | +| `simulation_engine` | string / null | موتور شبیه‌سازی | +| `simulation_model` | string / null | نام مدل | +| `scenario_id` | integer / null | شناسه سناریو | +| `simulation_warning` | string / null | هشدار شبیه‌سازی | +| `secondary_kpis_estimated` | boolean | آیا KPIهای ثانویه تخمینی‌اند | +| `descriptionSource` | string | منبع توضیح | +| `farm_context` | object | context مزرعه | +| `supporting_metrics` | object | متریک‌های پشتیبان | +| `explanation` | string | توضیح متنی | + +### توضیح `harvest_prediction_card` + +| فیلد | نوع | توضیح | +|---|---|---| +| `harvest_date` | string | تاریخ ISO برداشت | +| `harvest_date_formatted` | string | تاریخ قابل نمایش | +| `days_until` | integer | روز باقی‌مانده | +| `optimal_window_start` | string | شروع بازه مناسب | +| `optimal_window_end` | string | پایان بازه مناسب | +| `description` | string | توضیح متنی | +| `descriptionSource` | string | منبع توضیح | +| `field_conditions` | object | شرایط فعلی مزرعه | +| `readiness_metrics` | object | جزئیات readiness/GDD | + +### توضیح `harvest_readiness_zones` + +| فیلد | نوع | توضیح | +|---|---|---| +| `observationDate` | string / null | تاریخ مشاهده | +| `vegetationHealthClass` | string / null | کلاس سلامت پوشش گیاهی | +| `meanNdvi` | number / null | NDVI میانگین | +| `ndviTrend` | number / null | روند NDVI | +| `averageReadiness` | number / null | میانگین آمادگی | +| `zones` | array[object] | فهرست zoneها | +| `source` | string | منبع داده | +| `summary` | string | توضیح خلاصه | + +### توضیح هر zone در `zones[]` + +| فیلد | نوع | توضیح | +|---|---|---| +| `zoneId` | string | شناسه zone | +| `zoneLabel` | string | نام نمایشی zone | +| `gridPosition` | object / null | موقعیت grid | +| `meanNdvi` | number | NDVI میانگین zone | +| `readiness` | integer | درصد آمادگی | +| `daysUntil` | integer | روز باقی‌مانده | +| `status` | string | وضعیت zone | + +### توضیح `yield_quality_bands` + +| فیلد | نوع | توضیح | +|---|---|---| +| `source` | string | منبع محاسبه | +| `is_estimated` | boolean | آیا مقادیر تخمینی‌اند | +| `protein_content` | object | درصد پروتئین | +| `moisture_percentage` | object | درصد رطوبت | +| `grade_distribution` | array[object] | توزیع گریدها | +| `primary_quality_grade` | string | گرید غالب | +| `quality_score` | number | امتیاز کیفیت | +| `summary` | string | خلاصه متنی | + +### توضیح `harvest_operations_card` + +| فیلد | نوع | توضیح | +|---|---|---| +| `stage_label` | string | برچسب مرحله عملیاتی | +| `phase_name` | string | نام فاز رشد | +| `days_until_harvest` | integer | روز باقی‌مانده | +| `current_dvs` | number | DVS فعلی | +| `summary` | string | خلاصه عملیاتی | +| `rules_source` | string | منبع قواعد | +| `field_context` | object | context مزرعه | +| `steps` | array[object] | گام‌های عملیاتی | + +### توضیح هر step در `steps[]` + +| فیلد | نوع | توضیح | +|---|---|---| +| `key` | string | کلید فنی step | +| `title` | string | عنوان عملیات | +| `status` | string | وضعیت step | +| `is_completed` | boolean | آیا انجام شده | +| `estimated_days` | integer | روز برآوردی | +| `note` | string | توضیح تکمیلی | + +### توضیح `yield_prediction_chart` + +| فیلد | نوع | توضیح | +|---|---|---| +| `series` | array[object] | سری‌های نمودار | +| `xAxis` | object | تنظیمات محور افقی | +| `meta` | object | متادیتای نمودار | + +### توضیح `yield_prediction_chart.series[]` + +| فیلد | نوع | توضیح | +|---|---|---| +| `name` | string | نام سری | +| `type` | string | نوع رسم مانند `line` یا `area` | +| `data` | array[[timestamp, value]] | داده‌های نمودار؛ timestamp بر حسب milliseconds | + +### توضیح `yield_prediction_chart.meta` + +| فیلد | نوع | توضیح | +|---|---|---| +| `unit` | string | واحد داده | +| `simulation_engine` | string | موتور شبیه‌سازی | +| `simulation_model` | string | مدل | +| `scenario_id` | integer / null | شناسه سناریو | +| `simulation_warning` | string / null | هشدار شبیه‌سازی | +| `field_context` | object | context مزرعه | + +--- + +## خطاهای رایج با مثال + +### نبودن `farm_uuid` + +```json +{ + "code": 400, + "msg": "error", + "data": { + "farm_uuid": ["This field is required."] + } +} +``` + +### پیدا نشدن مزرعه برای کاربر جاری + +```json +{ + "code": 404, + "msg": "error", + "data": { + "farm_uuid": ["Farm not found."] + } +} +``` + +### خطای upstream AI + +```json +{ + "code": 500, + "msg": "error", + "data": { + "code": 500, + "msg": "خطا در پیش بینی عملکرد: Plant not found.", + "data": null + } +} +``` + +نکته: در این وضعیت، envelope بیرونی از backend آمده و object داخلی معمولاً همان پاسخ upstream AI است. + +--- + +## پیش‌فرض Swagger + +برای endpointهای body-based این ماژول، `farm_uuid` در Swagger با مقدار پیش‌فرض زیر نمایش داده می‌شود: + +```text +11111111-1111-1111-1111-111111111111 +``` + +این رفتار برای endpointهای زیر برقرار است: + +- `POST /api/yield-harvest/current-farm-chart/` +- `POST /api/yield-harvest/growth/` +- `POST /api/yield-harvest/harvest-prediction/` +- `POST /api/yield-harvest/yield-prediction/` + +--- + +## جمع‌بندی اجرایی برای فرانت + +### چیزی که فرانت باید بفرستد + +- برای بیشتر endpointها فقط `farm_uuid` +- برای status فقط `task_id` +- در جریان ساده، `plant_name` هرگز از کاربر گرفته نشود + +### چیزی که backend خودش مدیریت می‌کند + +- پیدا کردن مزرعه متعلق به کاربر +- استخراج `plant_name` از `farm.products` یا `farm.farm_type.products` +- ارسال payload مناسب به AI +- normalize کردن پاسخ AI در envelope استاندارد backend + +### چیزی که فرانت نباید به کاربر بسپارد + +- انتخاب دستی `plant_name` در این flow +- ساخت payload مستقیم AI +- تفسیر business ruleهای انتخاب محصول + +--- + +## مسیر فایل + +این سند در مسیر زیر نگهداری می‌شود: + +`docs/yield_harvest_ai_integration.md` diff --git a/farm_hub/catalog.py b/farm_hub/catalog.py index a36a36a..bddee2f 100644 --- a/farm_hub/catalog.py +++ b/farm_hub/catalog.py @@ -16,8 +16,8 @@ CATALOG_SEED_DATA = { {"name": "برنج", "planting_season": "بهار", "harvest_time": "اواخر تابستان", "soil": "رسی", "icon": "leaf", "growth_stage": "vegetative", "growth_stages": ["initial", "vegetative", "flowering", "maturity"]}, ], "گلخانه ای": [ - {"name": "گوجه فرنگی", "planting_season": "چهار فصل", "harvest_time": "چند مرحله ای", "soil": "کوکوپیت", "icon": "tomato", "growth_stage": "fruiting", "growth_stages": ["initial", "vegetative", "flowering", "fruiting", "maturity"]}, + {"name": "گوجه‌فرنگی", "planting_season": "چهار فصل", "harvest_time": "چند مرحله ای", "soil": "کوکوپیت", "icon": "tomato", "growth_stage": "fruiting", "growth_stages": ["initial", "vegetative", "flowering", "fruiting", "maturity"]}, {"name": "خیار", "planting_season": "چهار فصل", "harvest_time": "چند مرحله ای", "soil": "پرلیت", "icon": "leaf", "growth_stage": "fruiting", "growth_stages": ["initial", "vegetative", "flowering", "fruiting", "maturity"]}, - {"name": "فلفل دلمه ای", "planting_season": "چهار فصل", "harvest_time": "چند مرحله ای", "soil": "بستر هیدروپونیک", "icon": "pepper", "growth_stage": "flowering", "growth_stages": ["initial", "vegetative", "flowering", "fruiting", "maturity"]}, + {"name": "فلفل دلمه‌ای", "planting_season": "چهار فصل", "harvest_time": "چند مرحله ای", "soil": "بستر هیدروپونیک", "icon": "pepper", "growth_stage": "flowering", "growth_stages": ["initial", "vegetative", "flowering", "fruiting", "maturity"]}, ], } diff --git a/farm_hub/seeds.py b/farm_hub/seeds.py index 230b36e..e606e82 100644 --- a/farm_hub/seeds.py +++ b/farm_hub/seeds.py @@ -50,7 +50,7 @@ ADMIN_FARM_AREA_GEOJSON = { def _get_default_catalog(): - default_farm_type_name = "زراعی" + default_farm_type_name = "گلخانه ای" created_products = [] for farm_type_name, products in CATALOG_SEED_DATA.items(): diff --git a/yield_harvest/serializers.py b/yield_harvest/serializers.py index a021315..861ae8b 100644 --- a/yield_harvest/serializers.py +++ b/yield_harvest/serializers.py @@ -59,19 +59,32 @@ class YieldHarvestSummarySerializer(serializers.Serializer): class CropSimulationRequestSerializer(serializers.Serializer): - farm_uuid = serializers.UUIDField(required=True, help_text="UUID مزرعه برای اجرای شبیه‌سازی.") - plant_name = serializers.CharField(required=False, allow_blank=True, default="", help_text="نام گیاه یا محصول.") + farm_uuid = serializers.UUIDField( + required=True, + initial="11111111-1111-1111-1111-111111111111", + help_text="UUID مزرعه برای اجرای شبیه‌سازی.", + ) class GrowthSimulationRequestSerializer(serializers.Serializer): - plant_name = serializers.CharField(required=True, help_text="نام گیاه برای شروع شبیه‌سازی رشد.") + plant_name = serializers.CharField( + required=False, + allow_blank=True, + default="", + help_text="نام گیاه؛ اگر farm_uuid ارسال شود از محصول مزرعه استفاده می‌شود.", + ) dynamic_parameters = serializers.ListField( child=serializers.CharField(), required=True, allow_empty=False, help_text="لیست پارامترهای دینامیک موردنیاز مانند DVS یا LAI.", ) - farm_uuid = serializers.UUIDField(required=False, allow_null=True, help_text="UUID مزرعه؛ در صورت نبود باید weather ارسال شود.") + farm_uuid = serializers.UUIDField( + required=False, + allow_null=True, + initial="11111111-1111-1111-1111-111111111111", + help_text="UUID مزرعه؛ در صورت نبود باید weather ارسال شود.", + ) weather = serializers.JSONField(required=False, help_text="آب‌وهوا به‌صورت object یا array.") soil_parameters = serializers.DictField(required=False, help_text="پارامترهای خاک.") site_parameters = serializers.DictField(required=False, help_text="پارامترهای سایت.") @@ -82,6 +95,8 @@ class GrowthSimulationRequestSerializer(serializers.Serializer): def validate(self, attrs): if not attrs.get("farm_uuid") and attrs.get("weather") in (None, "", [], {}): raise serializers.ValidationError("At least one of 'farm_uuid' or 'weather' must be provided.") + if not attrs.get("farm_uuid") and not (attrs.get("plant_name") or "").strip(): + raise serializers.ValidationError({"plant_name": ["This field is required when farm_uuid is not provided."]}) return attrs diff --git a/yield_harvest/tests.py b/yield_harvest/tests.py index 8690354..6065bb5 100644 --- a/yield_harvest/tests.py +++ b/yield_harvest/tests.py @@ -5,7 +5,7 @@ from django.test import TestCase from rest_framework.test import APIClient, APIRequestFactory, force_authenticate from external_api_adapter.adapter import AdapterResponse -from farm_hub.models import FarmHub, FarmType +from farm_hub.models import FarmHub, FarmType, Product from .views import ( CurrentFarmChartView, @@ -36,6 +36,8 @@ class CropSimulationViewTests(TestCase): self.farm_type = FarmType.objects.create(name="زراعی") self.farm = FarmHub.objects.create(owner=self.user, farm_type=self.farm_type, name="Farm 1") self.other_farm = FarmHub.objects.create(owner=self.other_user, farm_type=self.farm_type, name="Farm 2") + self.product = Product.objects.create(farm_type=self.farm_type, name="گوجه‌فرنگی") + self.farm.products.add(self.product) self.api_client.force_authenticate(user=self.user) @patch("yield_harvest.views.external_api_request") @@ -100,6 +102,41 @@ class CropSimulationViewTests(TestCase): self.assertEqual(response.status_code, 202) self.assertEqual(response.json()["data"]["task_id"], "growth-task-123") + @patch("yield_harvest.views.external_api_request") + def test_growth_yield_harvest_route_queues_simulation_task(self, mock_external_api_request): + mock_external_api_request.return_value = AdapterResponse( + status_code=202, + data={ + "data": { + "task_id": "growth-task-123", + "status_url": "/api/crop-simulation/growth/growth-task-123/status/", + "plant_name": "گوجه‌فرنگی", + } + }, + ) + + response = self.api_client.post( + "/api/yield-harvest/growth/", + { + "plant_name": "wheat", + "dynamic_parameters": ["DVS", "LAI"], + "farm_uuid": str(self.farm.farm_uuid), + }, + format="json", + ) + + self.assertEqual(response.status_code, 202) + mock_external_api_request.assert_called_once_with( + "ai", + "/api/crop-simulation/growth/", + method="POST", + payload={ + "plant_name": "گوجه‌فرنگی", + "dynamic_parameters": ["DVS", "LAI"], + "farm_uuid": str(self.farm.farm_uuid), + }, + ) + def test_growth_requires_farm_uuid_or_weather(self): request = self.factory.post( "/api/yield-harvest/crop-simulation/growth/", @@ -171,6 +208,18 @@ class CropSimulationViewTests(TestCase): self.assertEqual(response.status_code, 200) self.assertEqual(response.json()["data"]["status"], "SUCCESS") + @patch("yield_harvest.views.external_api_request") + def test_growth_status_yield_harvest_route_proxies_to_ai_service(self, mock_external_api_request): + mock_external_api_request.return_value = AdapterResponse( + status_code=200, + data={"data": {"task_id": "growth-task-123", "status": "SUCCESS"}}, + ) + + response = self.api_client.get("/api/yield-harvest/growth/growth-task-123/status/?page=1&page_size=10") + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json()["data"]["status"], "SUCCESS") + def test_legacy_plant_simulator_routes_are_unavailable(self): legacy_paths = [ "/api/yield-harvest/plant-simulator/config/", @@ -193,7 +242,7 @@ class CropSimulationViewTests(TestCase): "data": { "result": { "farm_uuid": str(self.farm.farm_uuid), - "plant_name": "wheat", + "plant_name": "گوجه‌فرنگی", "scenario_id": 1, "categories": ["day1"], "series": {"biomass": [1.2]}, @@ -218,7 +267,7 @@ class CropSimulationViewTests(TestCase): "ai", "/api/crop-simulation/current-farm-chart/", method="POST", - payload={"farm_uuid": str(self.farm.farm_uuid), "plant_name": "wheat"}, + payload={"farm_uuid": str(self.farm.farm_uuid), "plant_name": "گوجه‌فرنگی"}, ) @patch("yield_harvest.views.external_api_request") @@ -237,6 +286,27 @@ class CropSimulationViewTests(TestCase): self.assertEqual(response.status_code, 200) self.assertEqual(response.json()["data"]["farm_uuid"], str(self.farm.farm_uuid)) + @patch("yield_harvest.views.external_api_request") + def test_current_farm_chart_yield_harvest_route_proxies_to_ai_service(self, mock_external_api_request): + mock_external_api_request.return_value = AdapterResponse( + status_code=200, + data={"data": {"result": {"farm_uuid": str(self.farm.farm_uuid), "plant_name": "گوجه‌فرنگی"}}}, + ) + + response = self.api_client.post( + "/api/yield-harvest/current-farm-chart/", + {"farm_uuid": str(self.farm.farm_uuid), "plant_name": "wheat"}, + format="json", + ) + + self.assertEqual(response.status_code, 200) + mock_external_api_request.assert_called_once_with( + "ai", + "/api/crop-simulation/current-farm-chart/", + method="POST", + payload={"farm_uuid": str(self.farm.farm_uuid), "plant_name": "گوجه‌فرنگی"}, + ) + @patch("yield_harvest.views.external_api_request") def test_harvest_prediction_proxies_to_ai_service(self, mock_external_api_request): mock_external_api_request.return_value = AdapterResponse( @@ -268,7 +338,7 @@ class CropSimulationViewTests(TestCase): "ai", "/api/crop-simulation/harvest-prediction/", method="POST", - payload={"farm_uuid": str(self.farm.farm_uuid), "plant_name": ""}, + payload={"farm_uuid": str(self.farm.farm_uuid), "plant_name": "گوجه‌فرنگی"}, ) @patch("yield_harvest.views.external_api_request") @@ -287,6 +357,27 @@ class CropSimulationViewTests(TestCase): self.assertEqual(response.status_code, 200) self.assertEqual(response.json()["data"]["daysUntil"], 96) + @patch("yield_harvest.views.external_api_request") + def test_harvest_prediction_yield_harvest_route_proxies_to_ai_service(self, mock_external_api_request): + mock_external_api_request.return_value = AdapterResponse( + status_code=200, + data={"data": {"result": {"date": "2026-07-15", "daysUntil": 96}}}, + ) + + response = self.api_client.post( + "/api/yield-harvest/harvest-prediction/", + {"farm_uuid": str(self.farm.farm_uuid)}, + format="json", + ) + + self.assertEqual(response.status_code, 200) + mock_external_api_request.assert_called_once_with( + "ai", + "/api/crop-simulation/harvest-prediction/", + method="POST", + payload={"farm_uuid": str(self.farm.farm_uuid), "plant_name": "گوجه‌فرنگی"}, + ) + @patch("yield_harvest.views.external_api_request") def test_yield_prediction_proxies_to_ai_service(self, mock_external_api_request): mock_external_api_request.return_value = AdapterResponse( @@ -313,6 +404,12 @@ class CropSimulationViewTests(TestCase): self.assertEqual(response.status_code, 200) self.assertEqual(response.data["data"]["predictedYieldTons"], 8.4) + mock_external_api_request.assert_called_once_with( + "ai", + "/api/crop-simulation/yield-prediction/", + method="POST", + payload={"farm_uuid": str(self.farm.farm_uuid), "plant_name": "گوجه‌فرنگی"}, + ) @patch("yield_harvest.views.external_api_request") def test_yield_prediction_top_level_route_proxies_to_ai_service(self, mock_external_api_request): @@ -330,6 +427,49 @@ class CropSimulationViewTests(TestCase): self.assertEqual(response.status_code, 200) self.assertEqual(response.json()["data"]["predictedYieldTons"], 8.4) + @patch("yield_harvest.views.external_api_request") + def test_yield_prediction_yield_harvest_route_proxies_to_ai_service(self, mock_external_api_request): + mock_external_api_request.return_value = AdapterResponse( + status_code=200, + data={"data": {"result": {"farm_uuid": str(self.farm.farm_uuid), "predictedYieldTons": 8.4}}}, + ) + + response = self.api_client.post( + "/api/yield-harvest/yield-prediction/", + {"farm_uuid": str(self.farm.farm_uuid)}, + format="json", + ) + + self.assertEqual(response.status_code, 200) + mock_external_api_request.assert_called_once_with( + "ai", + "/api/crop-simulation/yield-prediction/", + method="POST", + payload={"farm_uuid": str(self.farm.farm_uuid), "plant_name": "گوجه‌فرنگی"}, + ) + + @patch("yield_harvest.views.external_api_request") + def test_yield_prediction_falls_back_to_farm_type_product_when_farm_products_are_empty(self, mock_external_api_request): + farm_without_products = FarmHub.objects.create(owner=self.user, farm_type=self.farm_type, name="Farm fallback") + mock_external_api_request.return_value = AdapterResponse( + status_code=200, + data={"data": {"result": {"farm_uuid": str(farm_without_products.farm_uuid), "predictedYieldTons": 8.4}}}, + ) + + response = self.api_client.post( + "/api/yield-harvest/yield-prediction/", + {"farm_uuid": str(farm_without_products.farm_uuid)}, + format="json", + ) + + self.assertEqual(response.status_code, 200) + mock_external_api_request.assert_called_once_with( + "ai", + "/api/crop-simulation/yield-prediction/", + method="POST", + payload={"farm_uuid": str(farm_without_products.farm_uuid), "plant_name": "گوجه‌فرنگی"}, + ) + @patch("yield_harvest.views.external_api_request") def test_yield_harvest_summary_top_level_route_proxies_to_ai_service(self, mock_external_api_request): mock_external_api_request.return_value = AdapterResponse( @@ -368,6 +508,18 @@ class CropSimulationViewTests(TestCase): }, ) + @patch("yield_harvest.views.external_api_request") + def test_yield_harvest_summary_yield_harvest_route_proxies_to_ai_service(self, mock_external_api_request): + mock_external_api_request.return_value = AdapterResponse( + status_code=200, + data={"data": {"result": {"farm_uuid": str(self.farm.farm_uuid), "yield_prediction_chart": {"series": []}}}}, + ) + + response = self.api_client.get(f"/api/yield-harvest/yield-harvest-summary/?farm_uuid={self.farm.farm_uuid}") + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json()["data"]["farm_uuid"], str(self.farm.farm_uuid)) + def test_crop_simulation_rejects_foreign_farm_uuid(self): request = self.factory.post( "/api/yield-harvest/crop-simulation/yield-prediction/", diff --git a/yield_harvest/urls.py b/yield_harvest/urls.py index 42fc3ac..e613f27 100644 --- a/yield_harvest/urls.py +++ b/yield_harvest/urls.py @@ -11,10 +11,10 @@ from .views import ( urlpatterns = [ path("summary/", YieldHarvestSummaryView.as_view(), name="yield-harvest-summary"), - path("crop-simulation/current-farm-chart/", CurrentFarmChartView.as_view(), name="yield-harvest-current-farm-chart"), - path("crop-simulation/growth/", GrowthSimulationView.as_view(), name="yield-harvest-growth"), - path("crop-simulation/growth//status/", GrowthSimulationStatusView.as_view(), name="yield-harvest-growth-status"), - path("crop-simulation/harvest-prediction/", HarvestPredictionView.as_view(), name="yield-harvest-harvest-prediction"), - path("crop-simulation/yield-prediction/", YieldPredictionView.as_view(), name="yield-harvest-yield-prediction"), - path("crop-simulation/yield-harvest-summary/", YieldHarvestSummaryView.as_view(), name="yield-harvest-crop-simulation-summary"), + path("current-farm-chart/", CurrentFarmChartView.as_view(), name="yield-harvest-current-farm-chart"), + path("growth/", GrowthSimulationView.as_view(), name="yield-harvest-growth"), + path("growth//status/", GrowthSimulationStatusView.as_view(), name="yield-harvest-growth-status"), + path("harvest-prediction/", HarvestPredictionView.as_view(), name="yield-harvest-harvest-prediction"), + path("yield-prediction/", YieldPredictionView.as_view(), name="yield-harvest-yield-prediction"), + path("yield-harvest-summary/", YieldHarvestSummaryView.as_view(), name="yield-harvest-crop-simulation-summary"), ] diff --git a/yield_harvest/views.py b/yield_harvest/views.py index f96b963..c2b3f54 100644 --- a/yield_harvest/views.py +++ b/yield_harvest/views.py @@ -179,6 +179,18 @@ class CropSimulationBaseView(APIView): status=adapter_response.status_code, ) + @staticmethod + def _get_first_farm_product_name(farm): + first_product = farm.products.order_by("id").first() + if first_product is not None: + return (first_product.name or "").strip() + + fallback_product = farm.farm_type.products.order_by("id").first() + if fallback_product is not None: + return (fallback_product.name or "").strip() + + return "" + class CurrentFarmChartView(CropSimulationBaseView): ai_path = "/api/crop-simulation/current-farm-chart/" @@ -197,7 +209,10 @@ class CurrentFarmChartView(CropSimulationBaseView): if error_response is not None: return error_response - ai_payload = {"farm_uuid": str(farm.farm_uuid), "plant_name": payload.get("plant_name", "")} + ai_payload = { + "farm_uuid": str(farm.farm_uuid), + "plant_name": self._get_first_farm_product_name(farm), + } adapter_response = external_api_request("ai", self.ai_path, method="POST", payload=ai_payload) if adapter_response.status_code >= 400: @@ -226,7 +241,10 @@ class HarvestPredictionView(CropSimulationBaseView): if error_response is not None: return error_response - ai_payload = {"farm_uuid": str(farm.farm_uuid), "plant_name": payload.get("plant_name", "")} + ai_payload = { + "farm_uuid": str(farm.farm_uuid), + "plant_name": self._get_first_farm_product_name(farm), + } adapter_response = external_api_request("ai", self.ai_path, method="POST", payload=ai_payload) if adapter_response.status_code >= 400: @@ -255,7 +273,10 @@ class YieldPredictionView(CropSimulationBaseView): if error_response is not None: return error_response - ai_payload = {"farm_uuid": str(farm.farm_uuid), "plant_name": payload.get("plant_name", "")} + ai_payload = { + "farm_uuid": str(farm.farm_uuid), + "plant_name": self._get_first_farm_product_name(farm), + } adapter_response = external_api_request("ai", self.ai_path, method="POST", payload=ai_payload) if adapter_response.status_code >= 400: @@ -278,8 +299,13 @@ class GrowthSimulationView(APIView): serializer.is_valid(raise_exception=True) payload = serializer.validated_data.copy() - if payload.get("farm_uuid") is not None: - payload["farm_uuid"] = str(payload["farm_uuid"]) + farm_uuid = payload.get("farm_uuid") + if farm_uuid is not None: + farm, error_response = CropSimulationBaseView._get_farm(request, farm_uuid) + if error_response is not None: + return error_response + payload["farm_uuid"] = str(farm.farm_uuid) + payload["plant_name"] = CropSimulationBaseView._get_first_farm_product_name(farm) adapter_response = external_api_request( "ai",