221 lines
8.8 KiB
Python
221 lines
8.8 KiB
Python
from datetime import date, timedelta
|
|
from unittest.mock import patch
|
|
|
|
from django.test import TestCase, override_settings
|
|
from django.utils import timezone
|
|
from rest_framework.test import APIClient
|
|
|
|
from apps.accounts.models import ApiKey, User
|
|
from apps.farms.models import Cycle, Flock, Kandang, Site
|
|
from apps.operations.models import AIInsight, ChickenCounting, FeedSacks, IotPanel
|
|
|
|
|
|
SITE_KEY = "site-export-test-key"
|
|
|
|
|
|
@override_settings(DASHBOARD_PUBLISH_HOUR=17, TIME_ZONE="Asia/Jakarta")
|
|
class PusatExportTests(TestCase):
|
|
def setUp(self):
|
|
self.client = APIClient()
|
|
self.admin = User.objects.create_user(
|
|
user_name="export-admin",
|
|
password="x",
|
|
status=User.STATUS_SUPERADMIN,
|
|
)
|
|
api_key, _ = ApiKey.generate(self.admin, "site")
|
|
api_key.prefix = SITE_KEY[:8]
|
|
api_key.key_hash = ApiKey.hash_key(SITE_KEY)
|
|
api_key.save(update_fields=["prefix", "key_hash", "updated_at"])
|
|
|
|
self.site = Site.objects.create(site_name="ExportSite", user=self.admin)
|
|
self.other_site = Site.objects.create(site_name="OtherSite", user=self.admin)
|
|
self.kandang = Kandang.objects.create(kandang_name="K1", site=self.site)
|
|
self.other_kandang = Kandang.objects.create(kandang_name="K2", site=self.other_site)
|
|
self.flock = Flock.objects.create(flock_name="F1", kandang=self.kandang)
|
|
self.other_flock = Flock.objects.create(flock_name="F2", kandang=self.other_kandang)
|
|
|
|
today = timezone.localdate()
|
|
self.cycle = Cycle.objects.create(
|
|
kandang=self.kandang,
|
|
start_date=today - timedelta(days=10),
|
|
doc_in_weight=40,
|
|
doc_in_count=1000,
|
|
status=Cycle.STATUS_ACTIVE,
|
|
)
|
|
self.other_cycle = Cycle.objects.create(
|
|
kandang=self.other_kandang,
|
|
start_date=today - timedelta(days=10),
|
|
doc_in_weight=40,
|
|
doc_in_count=1000,
|
|
status=Cycle.STATUS_ACTIVE,
|
|
)
|
|
|
|
# "Today" rows would be hidden by publish cutoff on normal list APIs before 17:00.
|
|
FeedSacks.objects.create(cycle=self.cycle, date=today, in_today=1)
|
|
FeedSacks.objects.create(cycle=self.other_cycle, date=today, in_today=2)
|
|
ChickenCounting.objects.create(
|
|
cycle=self.cycle, date=today, total_count=100, mortality_count=1
|
|
)
|
|
AIInsight.objects.create(
|
|
cycle=self.cycle,
|
|
kandang=self.kandang,
|
|
date=today,
|
|
insight_text="ok",
|
|
alert="healthy",
|
|
topic="fcr",
|
|
report_type="page",
|
|
report_period="Hari 1",
|
|
)
|
|
IotPanel.objects.create(
|
|
flock=self.flock,
|
|
date=today,
|
|
timestamp=timezone.now(),
|
|
wind_speed=1.0,
|
|
humidity=50.0,
|
|
water_total=10.0,
|
|
average_temperature=28.0,
|
|
experience_temperature=27.0,
|
|
)
|
|
IotPanel.objects.create(
|
|
flock=self.other_flock,
|
|
date=today,
|
|
timestamp=timezone.now(),
|
|
wind_speed=1.0,
|
|
humidity=50.0,
|
|
water_total=10.0,
|
|
average_temperature=28.0,
|
|
experience_temperature=27.0,
|
|
)
|
|
|
|
def _auth(self):
|
|
self.client.credentials(HTTP_X_API_KEY=SITE_KEY)
|
|
|
|
def test_ops_requires_auth(self):
|
|
response = self.client.get("/api/v1/pusat/export/", {"site_id": self.site.pk})
|
|
self.assertEqual(response.status_code, 401)
|
|
|
|
def test_ops_requires_site_id(self):
|
|
self._auth()
|
|
response = self.client.get("/api/v1/pusat/export/")
|
|
self.assertEqual(response.status_code, 400)
|
|
|
|
def test_ops_unknown_site(self):
|
|
self._auth()
|
|
response = self.client.get("/api/v1/pusat/export/", {"site_id": "missing-9999"})
|
|
self.assertEqual(response.status_code, 400)
|
|
|
|
def test_ops_cycle_site_mismatch(self):
|
|
self._auth()
|
|
response = self.client.get(
|
|
"/api/v1/pusat/export/",
|
|
{"site_id": self.site.pk, "cycle_id": self.other_cycle.pk},
|
|
)
|
|
self.assertEqual(response.status_code, 400)
|
|
|
|
def test_ops_export_scopes_and_includes_today(self):
|
|
self._auth()
|
|
today = timezone.localdate().isoformat()
|
|
response = self.client.get(
|
|
"/api/v1/pusat/export/",
|
|
{"site_id": self.site.pk, "from": today, "to": today},
|
|
)
|
|
self.assertEqual(response.status_code, 200, response.data)
|
|
self.assertEqual(response.data["site_id"], self.site.pk)
|
|
self.assertEqual(len(response.data["sites"]), 1)
|
|
self.assertNotIn("flocks", response.data["sites"][0]["kandangs"][0])
|
|
self.assertNotIn("iot_panels", response.data["data"])
|
|
self.assertEqual(response.data["data"]["feed_sacks"]["count"], 1)
|
|
self.assertEqual(response.data["data"]["chicken_countings"]["count"], 1)
|
|
self.assertEqual(response.data["data"]["ai_insights"]["count"], 1)
|
|
self.assertEqual(
|
|
response.data["data"]["feed_sacks"]["results"][0]["site_id"], self.site.pk
|
|
)
|
|
|
|
def test_ops_cycle_id_defaults_to_cycle_span(self):
|
|
self._auth()
|
|
closed = Cycle.objects.create(
|
|
kandang=self.kandang,
|
|
start_date=date(2026, 1, 1),
|
|
end_date=date(2026, 2, 10),
|
|
doc_in_weight=40,
|
|
doc_in_count=1000,
|
|
status=Cycle.STATUS_CLOSED,
|
|
)
|
|
FeedSacks.objects.create(cycle=closed, date=date(2026, 1, 15), in_today=3)
|
|
response = self.client.get(
|
|
"/api/v1/pusat/export/",
|
|
{"site_id": self.site.pk, "cycle_id": closed.pk},
|
|
)
|
|
self.assertEqual(response.status_code, 200, response.data)
|
|
self.assertEqual(response.data["from"], "2026-01-01")
|
|
self.assertEqual(response.data["to"], "2026-02-10")
|
|
self.assertEqual(response.data["data"]["feed_sacks"]["count"], 1)
|
|
|
|
def test_ops_export_includes_initial_balance_iot(self):
|
|
self._auth()
|
|
today = timezone.localdate()
|
|
self.cycle.feed_initial_balance = 120
|
|
self.cycle.feed_initial_balance_date = today
|
|
self.cycle.save(
|
|
update_fields=["feed_initial_balance", "feed_initial_balance_date", "updated_at"]
|
|
)
|
|
with patch(
|
|
"apps.farms.services.pusat_export.pull_initial_balance_iot",
|
|
return_value=(115, None),
|
|
) as mock_pull:
|
|
response = self.client.get(
|
|
"/api/v1/pusat/export/",
|
|
{"site_id": self.site.pk, "cycle_id": self.cycle.pk},
|
|
)
|
|
self.assertEqual(response.status_code, 200, response.data)
|
|
cycle_row = response.data["sites"][0]["kandangs"][0]["cycles"][0]
|
|
self.assertEqual(cycle_row["feed_initial_balance"], 120)
|
|
self.assertEqual(cycle_row["feed_initial_balance_date"], today.isoformat())
|
|
self.assertEqual(cycle_row["feed_initial_balance_iot"], 115)
|
|
self.assertEqual(cycle_row["feed_initial_balance_accuracy"], 95.8)
|
|
self.assertIsNone(cycle_row["feed_initial_balance_sync_error"])
|
|
mock_pull.assert_called_once_with(self.cycle, today)
|
|
|
|
def test_ops_export_skips_iot_pull_without_balance_date(self):
|
|
self._auth()
|
|
with patch(
|
|
"apps.farms.services.pusat_export.pull_initial_balance_iot",
|
|
return_value=(99, None),
|
|
) as mock_pull:
|
|
response = self.client.get(
|
|
"/api/v1/pusat/export/",
|
|
{"site_id": self.site.pk, "cycle_id": self.cycle.pk},
|
|
)
|
|
self.assertEqual(response.status_code, 200, response.data)
|
|
cycle_row = response.data["sites"][0]["kandangs"][0]["cycles"][0]
|
|
self.assertIsNone(cycle_row["feed_initial_balance_iot"])
|
|
self.assertIsNone(cycle_row["feed_initial_balance_accuracy"])
|
|
self.assertIsNone(cycle_row["feed_initial_balance_sync_error"])
|
|
mock_pull.assert_not_called()
|
|
|
|
|
|
def test_iot_flock_site_mismatch(self):
|
|
self._auth()
|
|
response = self.client.get(
|
|
"/api/v1/pusat/export/iot/",
|
|
{"site_id": self.site.pk, "flock_id": self.other_flock.pk},
|
|
)
|
|
self.assertEqual(response.status_code, 400)
|
|
|
|
def test_iot_export_structure_and_panels(self):
|
|
self._auth()
|
|
today = timezone.localdate().isoformat()
|
|
response = self.client.get(
|
|
"/api/v1/pusat/export/iot/",
|
|
{"site_id": self.site.pk, "from": today, "to": today},
|
|
)
|
|
self.assertEqual(response.status_code, 200, response.data)
|
|
site_node = response.data["sites"][0]
|
|
self.assertEqual(site_node["site_id"], self.site.pk)
|
|
self.assertIn("flocks", site_node["kandangs"][0])
|
|
self.assertNotIn("cycles", site_node["kandangs"][0])
|
|
self.assertEqual(response.data["data"]["iot_panels"]["count"], 1)
|
|
self.assertEqual(
|
|
response.data["data"]["iot_panels"]["results"][0]["site_id"], self.site.pk
|
|
)
|