255 lines
8.7 KiB
Python
255 lines
8.7 KiB
Python
from decimal import Decimal
|
|
|
|
from django.core.cache import cache
|
|
from django.test import TestCase
|
|
from rest_framework.test import APITestCase
|
|
|
|
from apps.projects.models import Project
|
|
from apps.time_entries.services.rates import resolve_rate
|
|
from apps.users.models import User
|
|
from apps.workspaces.models import (
|
|
PriceUnit,
|
|
Workspace,
|
|
WorkspaceMembership,
|
|
WorkspaceUserRate,
|
|
)
|
|
from apps.workspaces.services.rates import (
|
|
update_workspace_user_rate,
|
|
upsert_workspace_user_rate,
|
|
)
|
|
|
|
|
|
class WorkspaceRateTests(APITestCase):
|
|
@classmethod
|
|
def setUpTestData(cls):
|
|
cls.owner = User.objects.create_user(mobile="09127770001", password="secret123")
|
|
cls.admin = User.objects.create_user(mobile="09127770002", password="secret123")
|
|
cls.member = User.objects.create_user(mobile="09127770003", password="secret123")
|
|
|
|
cls.workspace = Workspace.objects.create(name="Rates", owner=cls.owner)
|
|
WorkspaceMembership.objects.create(
|
|
workspace=cls.workspace,
|
|
user=cls.admin,
|
|
role=WorkspaceMembership.Role.ADMIN,
|
|
is_active=True,
|
|
)
|
|
WorkspaceMembership.objects.create(
|
|
workspace=cls.workspace,
|
|
user=cls.member,
|
|
role=WorkspaceMembership.Role.MEMBER,
|
|
is_active=True,
|
|
)
|
|
cls.project = Project.objects.create(workspace=cls.workspace, name="Billing")
|
|
|
|
PriceUnit.objects.create(
|
|
code="USD",
|
|
name="US Dollar",
|
|
local_name="Dollar",
|
|
symbol="$",
|
|
)
|
|
PriceUnit.objects.create(
|
|
code="EUR",
|
|
name="Euro",
|
|
local_name="Euro",
|
|
symbol="EUR",
|
|
)
|
|
|
|
def setUp(self):
|
|
cache.clear()
|
|
|
|
def test_resolve_rate_uses_workspace_user_rate(self):
|
|
WorkspaceUserRate.objects.create(
|
|
workspace=self.workspace,
|
|
user=self.member,
|
|
hourly_rate=Decimal("40.00"),
|
|
currency="EUR",
|
|
effective_from=self.project.created_at,
|
|
is_active=True,
|
|
)
|
|
|
|
hourly_rate, currency = resolve_rate(self.member, self.project)
|
|
|
|
self.assertEqual(hourly_rate, Decimal("40.00"))
|
|
self.assertEqual(currency, "EUR")
|
|
|
|
def test_resolve_rate_returns_none_when_workspace_rate_is_missing(self):
|
|
hourly_rate, currency = resolve_rate(self.member, self.project)
|
|
|
|
self.assertIsNone(hourly_rate)
|
|
self.assertEqual(currency, "")
|
|
|
|
def test_admin_can_manage_workspace_user_rates(self):
|
|
self.client.force_authenticate(user=self.admin)
|
|
|
|
create_response = self.client.post(
|
|
"/api/workspace-user-rates/",
|
|
{
|
|
"workspace_id": str(self.workspace.id),
|
|
"user_id": str(self.member.id),
|
|
"hourly_rate": "35.50",
|
|
"currency": "USD",
|
|
},
|
|
format="json",
|
|
)
|
|
|
|
self.assertEqual(create_response.status_code, 201)
|
|
rate_id = create_response.data["id"]
|
|
self.assertTrue(
|
|
WorkspaceUserRate.objects.filter(id=rate_id, is_deleted=False).exists()
|
|
)
|
|
|
|
update_response = self.client.patch(
|
|
f"/api/workspace-user-rates/{rate_id}/",
|
|
{"hourly_rate": "42.00"},
|
|
format="json",
|
|
)
|
|
self.assertEqual(update_response.status_code, 200)
|
|
self.assertEqual(update_response.data["hourly_rate"], "42.00")
|
|
|
|
delete_response = self.client.delete(f"/api/workspace-user-rates/{rate_id}/")
|
|
self.assertEqual(delete_response.status_code, 204)
|
|
self.assertTrue(WorkspaceUserRate.all_objects.get(id=rate_id).is_deleted)
|
|
|
|
def test_member_cannot_manage_rates(self):
|
|
self.client.force_authenticate(user=self.member)
|
|
|
|
response = self.client.post(
|
|
"/api/workspace-user-rates/",
|
|
{
|
|
"workspace_id": str(self.workspace.id),
|
|
"user_id": str(self.member.id),
|
|
"hourly_rate": "25.00",
|
|
"currency": "USD",
|
|
},
|
|
format="json",
|
|
)
|
|
|
|
self.assertEqual(response.status_code, 403)
|
|
|
|
def test_workspace_user_rates_cache_invalidates_after_rate_save(self):
|
|
rate = WorkspaceUserRate.objects.create(
|
|
workspace=self.workspace,
|
|
user=self.member,
|
|
hourly_rate=Decimal("30.00"),
|
|
currency="USD",
|
|
effective_from=self.workspace.created_at,
|
|
is_active=True,
|
|
)
|
|
self.client.force_authenticate(user=self.admin)
|
|
|
|
first_response = self.client.get(
|
|
"/api/workspace-user-rates/",
|
|
{"workspace": str(self.workspace.id)},
|
|
)
|
|
self.assertEqual(first_response.status_code, 200)
|
|
self.assertEqual(first_response.data["items"][0]["hourly_rate"], "30.00")
|
|
|
|
WorkspaceUserRate.objects.filter(id=rate.id).update(hourly_rate=Decimal("45.00"))
|
|
|
|
cached_response = self.client.get(
|
|
"/api/workspace-user-rates/",
|
|
{"workspace": str(self.workspace.id)},
|
|
)
|
|
self.assertEqual(cached_response.status_code, 200)
|
|
self.assertEqual(cached_response.data["items"][0]["hourly_rate"], "30.00")
|
|
|
|
rate.refresh_from_db()
|
|
rate.currency = "EUR"
|
|
rate.save(update_fields=["currency"])
|
|
|
|
fresh_response = self.client.get(
|
|
"/api/workspace-user-rates/",
|
|
{"workspace": str(self.workspace.id)},
|
|
)
|
|
self.assertEqual(fresh_response.status_code, 200)
|
|
self.assertEqual(fresh_response.data["items"][0]["hourly_rate"], "45.00")
|
|
self.assertEqual(fresh_response.data["items"][0]["currency"], "EUR")
|
|
|
|
def test_price_unit_cache_invalidates_after_price_unit_create(self):
|
|
self.client.force_authenticate(user=self.owner)
|
|
|
|
first_response = self.client.get("/api/price-units/")
|
|
self.assertEqual(first_response.status_code, 200)
|
|
self.assertEqual(first_response.data[0]["name"], "Euro")
|
|
self.assertEqual(len(first_response.data), 2)
|
|
|
|
PriceUnit.objects.filter(code="EUR").update(name="Updated Euro")
|
|
|
|
cached_response = self.client.get("/api/price-units/")
|
|
self.assertEqual(cached_response.status_code, 200)
|
|
self.assertEqual(cached_response.data[0]["name"], "Euro")
|
|
|
|
PriceUnit.objects.create(
|
|
code="GBP",
|
|
name="British Pound",
|
|
local_name="Pound",
|
|
symbol="£",
|
|
)
|
|
|
|
fresh_response = self.client.get("/api/price-units/")
|
|
self.assertEqual(fresh_response.status_code, 200)
|
|
self.assertEqual(len(fresh_response.data), 3)
|
|
euro_row = next(item for item in fresh_response.data if item["code"] == "EUR")
|
|
self.assertEqual(euro_row["name"], "Updated Euro")
|
|
|
|
|
|
class WorkspaceRateServiceTests(TestCase):
|
|
@classmethod
|
|
def setUpTestData(cls):
|
|
cls.owner = User.objects.create_user(mobile="09127770011", password="secret123")
|
|
cls.member = User.objects.create_user(mobile="09127770012", password="secret123")
|
|
cls.workspace = Workspace.objects.create(name="Rate Services", owner=cls.owner)
|
|
|
|
def test_upsert_workspace_user_rate_creates_uppercase_currency_rate(self):
|
|
rate = upsert_workspace_user_rate(
|
|
self.workspace,
|
|
self.member.id,
|
|
Decimal("12.50"),
|
|
"usd",
|
|
)
|
|
|
|
self.assertEqual(rate.hourly_rate, Decimal("12.50"))
|
|
self.assertEqual(rate.currency, "USD")
|
|
self.assertTrue(rate.is_active)
|
|
|
|
def test_upsert_workspace_user_rate_updates_existing_inactive_rate(self):
|
|
rate = WorkspaceUserRate.objects.create(
|
|
workspace=self.workspace,
|
|
user=self.member,
|
|
hourly_rate=Decimal("10.00"),
|
|
currency="USD",
|
|
effective_from=self.workspace.created_at,
|
|
is_active=False,
|
|
)
|
|
|
|
updated = upsert_workspace_user_rate(
|
|
self.workspace,
|
|
self.member.id,
|
|
Decimal("20.00"),
|
|
"eur",
|
|
)
|
|
|
|
self.assertEqual(updated.id, rate.id)
|
|
self.assertEqual(updated.hourly_rate, Decimal("20.00"))
|
|
self.assertEqual(updated.currency, "EUR")
|
|
self.assertTrue(updated.is_active)
|
|
|
|
def test_update_workspace_user_rate_updates_only_changed_fields(self):
|
|
rate = WorkspaceUserRate.objects.create(
|
|
workspace=self.workspace,
|
|
user=self.member,
|
|
hourly_rate=Decimal("10.00"),
|
|
currency="USD",
|
|
effective_from=self.workspace.created_at,
|
|
is_active=True,
|
|
)
|
|
|
|
updated = update_workspace_user_rate(
|
|
rate,
|
|
hourly_rate=Decimal("15.00"),
|
|
currency="gbp",
|
|
)
|
|
|
|
self.assertEqual(updated.hourly_rate, Decimal("15.00"))
|
|
self.assertEqual(updated.currency, "GBP")
|