diff --git a/apps/discord_bot/src/five08/discord_bot/cogs/invoices.py b/apps/discord_bot/src/five08/discord_bot/cogs/invoices.py index 5216da71..34d63d35 100644 --- a/apps/discord_bot/src/five08/discord_bot/cogs/invoices.py +++ b/apps/discord_bot/src/five08/discord_bot/cogs/invoices.py @@ -27,10 +27,10 @@ STATUS_LABEL = {0: "Draft", 1: "Submitted", 2: "Cancelled"} # Invoice access rules — a caller may validate an invoice if any of these hold: -# 1. They have Steering Committee role or above (full access). +# 1. They have a privileged role (one of _PRIVILEGED_ROLES) for full access. # 2. They created the invoice (invoice owner matches one of their ERP emails). # 3. They are on the invoice's ERP project roster. -_PRIVILEGED_ROLES = ["Steering Committee"] +_PRIVILEGED_ROLES = ["Steering Committee", "Workflows Engineer"] def _can_view_invoice( @@ -115,8 +115,9 @@ async def validate_invoice_command( self._resolve_access, interaction ) if not include_all and not emails: + roles_str = " or ".join(_PRIVILEGED_ROLES) await interaction.followup.send( - "Invoice validation is available to Steering Committee members " + f"Invoice validation is restricted to {roles_str} " "or confirmed ERP project members.", ephemeral=True, ) diff --git a/tests/unit/test_invoices_cog.py b/tests/unit/test_invoices_cog.py index 80df1a57..2816037d 100644 --- a/tests/unit/test_invoices_cog.py +++ b/tests/unit/test_invoices_cog.py @@ -4,7 +4,7 @@ import pytest -from five08.discord_bot.cogs.invoices import InvoicesCog +from five08.discord_bot.cogs.invoices import InvoicesCog, _PRIVILEGED_ROLES from five08.clients.erpnext import ERPNextAPIError @@ -45,6 +45,12 @@ def mock_interaction() -> AsyncMock: return _make_interaction(role_names=["Steering Committee"], user_id=1001) +@pytest.fixture +def mock_workflows_engineer_interaction() -> AsyncMock: + """A privileged (Workflows Engineer) caller with full invoice access.""" + return _make_interaction(role_names=["Workflows Engineer"], user_id=1002) + + @pytest.fixture def mock_member_interaction() -> AsyncMock: """A non-privileged caller subject to owner/project access rules.""" @@ -148,10 +154,23 @@ async def test_validate_invoice_denied_without_erp_identity( cog, mock_member_interaction, mock_doctype, "TEST-SINV-0001" ) sent = mock_member_interaction.followup.send.call_args.args[0] - assert "Steering Committee" in sent + assert all(role in sent for role in _PRIVILEGED_ROLES) cog.client.get_invoice.assert_not_called() +@pytest.mark.asyncio +async def test_validate_invoice_allowed_for_workflows_engineer( + cog, mock_workflows_engineer_interaction, mock_doctype +): + """Workflows Engineer gets include_all access without needing an ERP identity.""" + cog.client.get_invoice = Mock(return_value=VALID_INVOICE) + await cog.validate_invoice_command.callback( + cog, mock_workflows_engineer_interaction, mock_doctype, "TEST-SINV-0001" + ) + embed = mock_workflows_engineer_interaction.followup.send.call_args.kwargs["embed"] + assert "No issues found" in embed.title + + @pytest.mark.asyncio async def test_validate_invoice_allowed_for_invoice_owner( cog, mock_member_interaction, mock_doctype