update api export for executive and fixing closing cycle flow
This commit is contained in:
1 parent
d0d1ae7d24
commit
07d36081ce
23 files changed
+1378
-101
No files matched your search
@@ -0,0 +1,218 @@
|
||||
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,
|
||||
date=today,
|
||||
insight_text="ok",
|
||||
alert="none",
|
||||
section="fcr",
|
||||
session="morning",
|
||||
)
|
||||
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
|
||||
)
|
||||
Reference in new issue
Block a user