feat(projects): add project-specific member rates

This commit is contained in:
2026-05-23 18:29:00 +03:30
parent b79fd73403
commit 181a135df9
8 changed files with 381 additions and 22 deletions

View File

@@ -1,9 +1,11 @@
from decimal import Decimal
from rest_framework.test import APITestCase
from apps.clients.models import Client
from apps.projects.models import Project, ProjectAccess
from apps.projects.models import Project, ProjectAccess, ProjectUserRate
from apps.users.models import User
from apps.workspaces.models import Workspace, WorkspaceMembership
from apps.workspaces.models import PriceUnit, Workspace, WorkspaceMembership, WorkspaceUserRate
class ProjectViewTests(APITestCase):
@@ -15,6 +17,7 @@ class ProjectViewTests(APITestCase):
first_name="Owner",
)
cls.workspace = Workspace.objects.create(name="Projects", owner=cls.owner)
PriceUnit.objects.create(code="USD", name="US Dollar", local_name="Dollar", symbol="$")
cls.member = User.objects.create_user(
mobile="09121110002",
password="secret123",
@@ -47,6 +50,14 @@ class ProjectViewTests(APITestCase):
cls.first_project = Project.objects.get(name="Alpha")
ProjectAccess.objects.create(project=cls.first_project, user=cls.member)
ProjectAccess.objects.create(project=cls.second_project, user=cls.member)
WorkspaceUserRate.objects.create(
workspace=cls.workspace,
user=cls.member,
hourly_rate=Decimal("25.00"),
currency="USD",
effective_from=cls.workspace.created_at,
is_active=True,
)
def test_project_list_supports_multi_client_filter(self):
self.client.force_authenticate(user=self.member)
@@ -84,6 +95,9 @@ class ProjectViewTests(APITestCase):
items = access_response.data["items"]
gamma_item = next(item for item in items if item["id"] == str(self.third_project.id))
self.assertFalse(gamma_item["has_access"])
alpha_item = next(item for item in items if item["id"] == str(self.first_project.id))
self.assertEqual(alpha_item["workspace_rate"]["hourly_rate"], "25.00")
self.assertIsNone(alpha_item["project_rate"])
grant_response = self.client.post(
"/api/projects/access/grant/",
@@ -114,3 +128,66 @@ class ProjectViewTests(APITestCase):
)
self.assertEqual(revoke_response.status_code, 200)
self.assertFalse(ProjectAccess.objects.filter(project=self.first_project, user=self.member).exists())
def test_project_access_rate_endpoint_saves_override_and_keeps_it_dormant_after_revoke(self):
self.client.force_authenticate(user=self.owner)
save_response = self.client.post(
"/api/projects/access/rate/",
{
"workspace": str(self.workspace.id),
"user": str(self.member.id),
"project": str(self.first_project.id),
"hourly_rate": "44.50",
"currency": "USD",
},
format="json",
)
self.assertEqual(save_response.status_code, 200)
self.assertFalse(save_response.data["removed"])
self.assertEqual(save_response.data["item"]["project_rate"]["hourly_rate"], "44.50")
self.assertTrue(
ProjectUserRate.objects.filter(project=self.first_project, user=self.member, is_deleted=False).exists()
)
revoke_response = self.client.post(
"/api/projects/access/revoke/",
{
"workspace": str(self.workspace.id),
"user": str(self.member.id),
"project_ids": [str(self.first_project.id)],
},
format="json",
)
self.assertEqual(revoke_response.status_code, 200)
self.assertTrue(
ProjectUserRate.objects.filter(project=self.first_project, user=self.member, is_deleted=False).exists()
)
access_response = self.client.get(
"/api/projects/access/",
{"workspace": str(self.workspace.id), "user": str(self.member.id)},
)
self.assertEqual(access_response.status_code, 200)
alpha_item = next(item for item in access_response.data["items"] if item["id"] == str(self.first_project.id))
self.assertFalse(alpha_item["has_access"])
self.assertEqual(alpha_item["project_rate"]["hourly_rate"], "44.50")
def test_project_access_rate_endpoint_rejects_projects_without_access(self):
self.client.force_authenticate(user=self.owner)
response = self.client.post(
"/api/projects/access/rate/",
{
"workspace": str(self.workspace.id),
"user": str(self.member.id),
"project": str(self.third_project.id),
"hourly_rate": "44.50",
"currency": "USD",
},
format="json",
)
self.assertEqual(response.status_code, 400)
self.assertIn("Grant project access", response.data["detail"])