Files
dashboard-cpsp-executive/backend/apps/farms/tests.py
T

314 lines
12 KiB
Python

from datetime import date, timedelta
from unittest.mock import patch
from django.test import TestCase
from rest_framework.test import APIClient
from apps.accounts.models import User
from apps.farms.models import Cycle, Flock, Kandang, Site
from apps.sync.models import ActiveSite
def _gm_visible_site(*, owner: User, site_name: str, code: str) -> Site:
"""GM scope only sees sites linked to an ActiveSite they manage."""
site_id = f"{code}-0001"
active_site = ActiveSite.objects.create(
code=code,
name=site_name,
source_site_id=site_id,
api_base_url=f"https://{code}.example/api/v1",
managed_by=owner,
)
return Site.objects.create(
site_id=site_id,
site_name=site_name,
user=owner,
active_site=active_site,
)
class FarmAPITests(TestCase):
def setUp(self):
self.user = User.objects.create_user(user_name="tester", password="secret")
self.admin = User.objects.create_user(
user_name="admin",
password="secret",
status=User.STATUS_DIRECTOR_ADMIN,
)
self.client = APIClient()
self.site = _gm_visible_site(
owner=self.user, site_name="Sukawarna", code="sukawarna"
)
self.kandang = Kandang.objects.create(kandang_name="Kandang 1", site=self.site)
def test_create_flock_under_kandang_requires_director_admin(self):
self.client.force_authenticate(self.admin)
response = self.client.post(
"/api/v1/flocks/",
{"flock_name": "Lantai 1", "kandang": self.kandang.pk},
format="json",
)
self.assertEqual(response.status_code, 201, response.data)
self.assertEqual(response.data["flock_name"], "Lantai 1")
self.assertEqual(response.data["kandang"], self.kandang.pk)
def test_staff_cannot_create_flock(self):
self.client.force_authenticate(self.user)
response = self.client.post(
"/api/v1/flocks/",
{"flock_name": "Lantai 1", "kandang": self.kandang.pk},
format="json",
)
self.assertEqual(response.status_code, 403, response.data)
def test_create_cycle_without_flock(self):
self.client.force_authenticate(self.user)
start = date.today()
response = self.client.post(
"/api/v1/cycles/",
{
"kandang": self.kandang.pk,
"total_days": 35,
"start_date": start.isoformat(),
"end_date": (start + timedelta(days=34)).isoformat(),
"chick_in_weight": 42,
"doc_in_count": 10000,
"status": "active",
},
format="json",
)
self.assertEqual(response.status_code, 201, response.data)
self.assertNotIn("flock", response.data)
self.assertNotIn("flock_name", response.data)
def test_update_cycle(self):
self.client.force_authenticate(self.user)
start = date(2026, 8, 1)
cycle = Cycle.objects.create(
kandang=self.kandang,
total_days=35,
start_date=start,
end_date=start + timedelta(days=34),
doc_in_weight=40,
doc_in_count=9000,
status=Cycle.STATUS_ACTIVE,
)
response = self.client.patch(
f"/api/v1/cycles/{cycle.pk}/",
{
"doc_in_count": 9500,
"chick_in_weight": 42,
},
format="json",
)
self.assertEqual(response.status_code, 200, response.data)
cycle.refresh_from_db()
# end_date is not writable via PATCH (close flow uses approve-close).
self.assertEqual(cycle.end_date, start + timedelta(days=34))
self.assertEqual(cycle.doc_in_count, 9500)
self.assertEqual(cycle.doc_in_weight, 42)
def test_cycle_stays_active_without_gm_approval(self):
"""HQ does not auto-close by publish cutoff — GM approve-close is required."""
self.client.force_authenticate(self.user)
start = date(2026, 8, 1)
cycle = Cycle.objects.create(
kandang=self.kandang,
total_days=35,
start_date=start,
end_date=None,
doc_in_weight=40,
doc_in_count=9000,
status=Cycle.STATUS_ACTIVE,
)
response = self.client.get(f"/api/v1/cycles/{cycle.pk}/")
self.assertEqual(response.status_code, 200, response.data)
self.assertEqual(response.data["status"], Cycle.STATUS_ACTIVE)
cycle.refresh_from_db()
self.assertEqual(cycle.status, Cycle.STATUS_ACTIVE)
def test_gm_can_approve_cycle_close(self):
gm = User.objects.create_user(user_name="gm_approver", password="x", status=User.STATUS_GM)
site = _gm_visible_site(owner=gm, site_name="GM Site", code="gm-approve")
kandang = Kandang.objects.create(kandang_name="Kandang GM", site=site)
self.client.force_authenticate(gm)
start = date(2026, 8, 1)
proposed = start + timedelta(days=34)
cycle = Cycle.objects.create(
kandang=kandang,
total_days=1,
start_date=start,
end_date=None,
doc_in_weight=40,
doc_in_count=9000,
status=Cycle.STATUS_PENDING_CLOSE,
proposed_end_date=proposed,
)
response = self.client.post(f"/api/v1/cycles/{cycle.pk}/approve-close/")
self.assertEqual(response.status_code, 200, response.data)
body = response.data.get("cycle", response.data)
self.assertEqual(body["status"], Cycle.STATUS_CLOSED)
self.assertEqual(body["end_date"], proposed.isoformat())
cycle.refresh_from_db()
self.assertEqual(cycle.status, Cycle.STATUS_CLOSED)
self.assertEqual(cycle.close_approved_by_id, gm.pk)
self.assertIsNotNone(cycle.close_approved_at)
self.assertEqual(cycle.end_date, proposed)
def test_gm_can_reject_cycle_close(self):
gm = User.objects.create_user(user_name="gm_rejector", password="x", status=User.STATUS_GM)
site = _gm_visible_site(owner=gm, site_name="GM Reject Site", code="gm-reject")
kandang = Kandang.objects.create(kandang_name="Kandang R", site=site)
self.client.force_authenticate(gm)
start = date(2026, 8, 1)
cycle = Cycle.objects.create(
kandang=kandang,
total_days=1,
start_date=start,
end_date=None,
doc_in_weight=40,
doc_in_count=9000,
status=Cycle.STATUS_PENDING_CLOSE,
proposed_end_date=start + timedelta(days=20),
)
response = self.client.post(f"/api/v1/cycles/{cycle.pk}/reject-close/")
self.assertEqual(response.status_code, 200, response.data)
body = response.data.get("cycle", response.data)
self.assertEqual(body["status"], Cycle.STATUS_ACTIVE)
cycle.refresh_from_db()
self.assertEqual(cycle.status, Cycle.STATUS_ACTIVE)
self.assertIsNone(cycle.proposed_end_date)
def test_gm_admin_cannot_approve_cycle_close(self):
gm_admin = User.objects.create_user(
user_name="gm_admin_user", password="x", status=User.STATUS_GM_ADMIN
)
site = _gm_visible_site(
owner=gm_admin, site_name="GM Admin Site", code="gm-admin-site"
)
kandang = Kandang.objects.create(kandang_name="Kandang GMA", site=site)
self.client.force_authenticate(gm_admin)
start = date(2026, 8, 1)
cycle = Cycle.objects.create(
kandang=kandang,
total_days=1,
start_date=start,
end_date=None,
doc_in_weight=40,
doc_in_count=9000,
status=Cycle.STATUS_PENDING_CLOSE,
proposed_end_date=start + timedelta(days=34),
)
response = self.client.post(f"/api/v1/cycles/{cycle.pk}/approve-close/")
self.assertEqual(response.status_code, 403)
cycle.refresh_from_db()
self.assertEqual(cycle.status, Cycle.STATUS_PENDING_CLOSE)
def test_flock_persists_across_cycles(self):
flock = Flock.objects.create(flock_name="Lantai 1", kandang=self.kandang)
start = date.today()
for offset in (0, 40):
Cycle.objects.create(
kandang=self.kandang,
total_days=35,
start_date=start + timedelta(days=offset),
end_date=start + timedelta(days=offset + 34),
doc_in_weight=40,
doc_in_count=9000,
status=Cycle.STATUS_ACTIVE,
)
self.assertEqual(Flock.objects.filter(kandang=self.kandang).count(), 1)
self.assertEqual(flock.kandang_id, self.kandang.pk)
def test_current_day_is_zero_based_from_start_date(self):
start = date(2026, 9, 2)
cycle = Cycle.objects.create(
kandang=self.kandang,
total_days=35,
start_date=start,
end_date=start + timedelta(days=34),
doc_in_weight=40,
doc_in_count=9000,
status=Cycle.STATUS_ACTIVE,
)
with patch(
"apps.operations.services.visibility.visible_through_date",
return_value=start,
):
self.assertEqual(cycle.current_day, 0)
with patch(
"apps.operations.services.visibility.visible_through_date",
return_value=start + timedelta(days=1),
):
self.assertEqual(cycle.current_day, 1)
def test_list_flocks_by_kandang(self):
self.client.force_authenticate(self.user)
Flock.objects.create(flock_name="Lantai 1", kandang=self.kandang)
Flock.objects.create(flock_name="Lantai 2", kandang=self.kandang)
other_kandang = Kandang.objects.create(kandang_name="Kandang 2", site=self.site)
Flock.objects.create(flock_name="Other", kandang=other_kandang)
response = self.client.get("/api/v1/flocks/", {"kandang_id": self.kandang.pk})
self.assertEqual(response.status_code, 200, response.data)
results = response.data["results"] if isinstance(response.data, dict) else response.data
self.assertEqual(len(results), 2)
def test_initial_balance_write_endpoints_removed(self):
start = date.today()
cycle = Cycle.objects.create(
kandang=self.kandang,
total_days=35,
start_date=start,
end_date=start + timedelta(days=34),
doc_in_weight=40,
doc_in_count=9000,
status=Cycle.STATUS_ACTIVE,
)
self.client.force_authenticate(self.user)
post = self.client.post(
f"/api/v1/cycles/{cycle.pk}/initial-balance/",
{"date": start.isoformat(), "feed_in_manual": 120},
format="json",
)
compare = self.client.get(
f"/api/v1/cycles/{cycle.pk}/initial-balance-compare/",
{"date": start.isoformat(), "manual": 120},
)
self.assertEqual(post.status_code, 404)
self.assertEqual(compare.status_code, 404)
def test_cycle_api_exposes_mirrored_initial_balance_iot_fields(self):
start = date(2026, 8, 26)
cycle = Cycle.objects.create(
kandang=self.kandang,
total_days=35,
start_date=start,
end_date=start + timedelta(days=34),
doc_in_weight=40,
doc_in_count=9000,
feed_initial_balance=120,
feed_initial_balance_date=start,
feed_initial_balance_iot=115,
feed_initial_balance_accuracy=95.8,
feed_initial_balance_sync_error=None,
status=Cycle.STATUS_ACTIVE,
)
self.client.force_authenticate(self.user)
response = self.client.get(f"/api/v1/cycles/{cycle.pk}/")
self.assertEqual(response.status_code, 200, response.data)
self.assertEqual(response.data["feed_initial_balance"], 120)
self.assertEqual(response.data["feed_initial_balance_date"], start.isoformat())
self.assertEqual(response.data["feed_initial_balance_iot"], 115)
self.assertEqual(response.data["feed_initial_balance_accuracy"], 95.8)
self.assertIsNone(response.data["feed_initial_balance_sync_error"])