Skip to content

Add ERPNext invoice validation Discord command#290

Merged
michaelmwu merged 6 commits into
508-dev:mainfrom
lairwaves:feat/erpnext-invoice-validation
May 21, 2026
Merged

Add ERPNext invoice validation Discord command#290
michaelmwu merged 6 commits into
508-dev:mainfrom
lairwaves:feat/erpnext-invoice-validation

Conversation

@lairwaves
Copy link
Copy Markdown
Contributor

@lairwaves lairwaves commented May 19, 2026

Description

Adds /validate-invoice slash command to the Discord bot, backed by a
read-only ERPNext REST API client and invoice validation rules defined
by the community admin. Responses show an invoice summary (owner,
project, cost centre, dates) followed by actionable validation issues.

Summary

  • Adds ErpNextClient (read-only REST client) in packages/shared
  • Adds validate_invoice() with 3 rules: cost centre vs project, line-item cost centre/project consistency, purchase invoice due date (≥ 29 days from posting date)
  • Adds /validate-invoice Discord slash command with autocomplete, showing an invoice summary embed followed by flagged issues
  • Adds ERPNEXT_BASE_URL / ERPNEXT_API_TOKEN to config and .env.example
  • 27 unit tests (client, validation rules, cog commands)

Related Issue

From Discode #billing threads - Does the erp have an mcp or an api

How Has This Been Tested?

    1. Run pytest tests/unit/test_erpnext_client.py tests/unit/test_erpnext_cog.py
    1. Set ERPNEXT_BASE_URL and ERPNEXT_API_TOKEN in .env and start the bot
    1. Run /validate-invoice on a known good invoice → green embed with Invoice Info
    1. Run /validate-invoice on an invoice with cost centre mismatch → red embed with issue details
    1. Run /validate-invoice with a non-existent invoice number → "not found" message

Summary by CodeRabbit

  • New Features

    • Added /validate-invoice slash command with invoice-name autocomplete, ephemeral replies, authorization checks, invoice summary embed, and truncated “Issues” field to fit Discord limits.
    • Autocomplete shows status/owner/date and returns up to 25 choices.
    • Command only registers when ERPNext is configured.
  • New Validation

    • Invoice validation for Sales and Purchase invoices (project/cost-center rules, line-item consistency, purchase due-date rule).
  • Tests

    • Added comprehensive unit tests for client lookups, validation, command flows, autocomplete, error handling, and truncation.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 19, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds ERPNext client invoice retrieval/search, a validation module enforcing project/cost-center and purchase due-date rules, a Discord Cog exposing /validate-invoice with autocomplete and embed formatting, and unit tests covering client, validation, and Cog behavior.

Changes

ERPNext Invoice Validation Integration

Layer / File(s) Summary
Invoice validation rules module
packages/shared/src/five08/erpnext_validation.py
Adds ValidationIssue and ValidationResult dataclasses and validate_invoice(invoice, doctype) with checks: project→cost_center prefix, per-line project/cost_center matching, and Purchase Invoice due_date <posting_date + 29 days flagged.
Validation tests
tests/unit/test_erpnext_validation.py
Baseline Sales/Purchase invoice fixtures and tests covering valid cases, cost-center/project mismatches, line-item checks, due-date boundary and parsing behavior, and multi-issue reporting.
ERPNext HTTP client: invoice retrieval & search
packages/shared/src/five08/clients/erpnext.py
Adds ERPNextClient.get_invoice(doctype, name) returning dict or None on 404, and ERPNextClient.search_invoices(doctype, query, docstatus, limit, owners, projects) producing filtered, ordered results.
ERPNext client tests
tests/unit/test_erpnext_client.py
Fixtures and tests for get_invoice payload shape handling and search_invoices behavior including query filter, requested fields, and or_filters scoping.
Discord Bot Cog and Slash Command
apps/discord_bot/src/five08/discord_bot/cogs/erpnext.py
Adds ErpNextCog with /validate-invoice (ephemeral defer, thread-safe client calls, embed with status/owner/project/cost_center/dates, issues field truncated to Discord 1024 chars), invoice_name autocomplete (uses search_invoices), and conditional setup(bot) registration.
Discord Cog tests
tests/unit/test_erpnext_cog.py
Fixtures mocking Discord interaction and tests for command success, not-found and API-error handling, issues truncation, authorization checks, and autocomplete results/error cases.

Sequence Diagram

sequenceDiagram
  participant User
  participant Discord
  participant ErpNextCog
  participant ERPNextClient
  participant Validator
  User->>Discord: /validate-invoice (doctype, invoice_name)
  Discord->>ErpNextCog: interaction
  ErpNextCog->>ErpNextCog: defer(ephemeral)
  ErpNextCog->>ERPNextClient: get_invoice(doctype, name)
  ERPNextClient-->>ErpNextCog: invoice dict | None | error
  alt invoice found
    ErpNextCog->>Validator: validate_invoice(invoice, doctype)
    Validator-->>ErpNextCog: ValidationResult
    ErpNextCog->>Discord: followup(embed with fields/issues)
  else not found
    ErpNextCog->>Discord: followup("not found or you don't have access")
  else error
    ErpNextCog->>Discord: followup(error message)
  end
  Discord->>User: ephemeral followup visible to user
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I fetched invoices by moonlight and thread,
I checked projects, cost centers, and dates ahead,
I trimmed long issues so Discord won't dread,
Tests hop in to guard each rule I've said,
A hoppity bot keeps the ledgers well-fed.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.31% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately and concisely describes the main change: adding an ERPNext invoice validation Discord command. It directly corresponds to the primary objective of introducing a /validate-invoice slash command.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a6b323aea9

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread packages/shared/src/five08/erpnext_validation.py Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
packages/shared/src/five08/clients/erpnext.py (1)

39-86: ⚡ Quick win

Remove unused write operation support from read-only client.

The _request method supports POST, PATCH, and PUT (line 48), but the client is documented as "read-only" (line 15) and exposes no write methods. These unused code paths add cognitive load and could mislead future maintainers into thinking write operations are supported.

♻️ Proposed simplification

Since all public methods (get_invoice, search_invoices) use GET, simplify _request to only support GET:

     def _request(
         self,
         method: str,
         endpoint: str,
         params: dict[str, Any] | None = None,
     ) -> Any:
         url = f"{self.base_url}/api/resource/{endpoint}"
-        method = method.upper()
         try:
-            if method in ("POST", "PATCH", "PUT"):
-                response = self._session.request(
-                    method,
-                    url,
-                    json=params,
-                    timeout=self.timeout,
-                    verify=default_ca_bundle_path(),
-                )
-            else:
-                response = self._session.request(
-                    method,
-                    url,
-                    params=params,
-                    timeout=self.timeout,
-                    verify=default_ca_bundle_path(),
-                )
+            response = self._session.get(
+                url,
+                params=params,
+                timeout=self.timeout,
+                verify=default_ca_bundle_path(),
+            )

Alternatively, if future write support is planned, update the docstring to reflect this.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/shared/src/five08/clients/erpnext.py` around lines 39 - 86, The
_request method currently contains branching for POST/PATCH/PUT even though this
client is intended to be read-only and public methods (get_invoice,
search_invoices) only perform GETs; remove the unused write-branching in
_request by simplifying the call to always send parameters as query params
(i.e., call self._session.request(method, url, params=params, ...)) and drop the
json=params branch and any write-specific logic, keeping error handling intact;
if write support is planned later, instead update the class/docstring to not
claim read-only or add a clear TODO indicating planned write methods.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/discord_bot/src/five08/discord_bot/cogs/erpnext.py`:
- Around line 43-115: The validate_invoice_command handler must POST an audit
record to the worker audit ingest endpoint using AUDIT_API_BASE_URL and
API_SHARED_SECRET whenever a human triggers this validation; add an async HTTP
POST (e.g., via aiohttp) inside validate_invoice_command (referencing the
function name) after you determine the final outcome (after building/sending the
embed or immediately before interaction.followup.send) that sends a JSON payload
including actor (interaction.user.id/name), action ("validate_invoice"), doctype
(doctype.value), invoice_name, result (passed/failed and number of issues), and
timestamp, and authenticate the request with API_SHARED_SECRET (e.g., header
like Authorization or X-Api-Key as used by other audit calls); also ensure you
send an audit event in the ErpNextAPIError and generic Exception handlers so
failures are recorded as well.
- Around line 133-137: The autocomplete Choice.name built for
app_commands.Choice can exceed Discord's 100-char limit; modify the generator
that builds the label (currently using STATUS_LABEL.get(inv.get('docstatus'))
and inv['name']/inv.get('owner')/inv.get('posting_date')) to clamp the resulting
label to 100 characters before passing to app_commands.Choice. Concretely,
compose the label into a local variable, and if len(label) > 100 truncate it
(e.g. label = label[:97] + "..." to preserve length), then use that truncated
label as the Choice.name so Choice creation always meets Discord's 1-100 char
requirement.

In `@apps/discord_bot/src/five08/discord_bot/config.py`:
- Around line 68-70: Move the ERPNext config fields out of the service-specific
Settings and into the shared settings: remove erpnext_base_url and
erpnext_api_token from the Discord bot's Settings class and add those two fields
to the SharedSettings class so all services reuse them; update no logic
here—just relocate the declarations (erpnext_base_url, erpnext_api_token) into
SharedSettings and rely on the existing inheritance of Settings to pick them up.

---

Nitpick comments:
In `@packages/shared/src/five08/clients/erpnext.py`:
- Around line 39-86: The _request method currently contains branching for
POST/PATCH/PUT even though this client is intended to be read-only and public
methods (get_invoice, search_invoices) only perform GETs; remove the unused
write-branching in _request by simplifying the call to always send parameters as
query params (i.e., call self._session.request(method, url, params=params, ...))
and drop the json=params branch and any write-specific logic, keeping error
handling intact; if write support is planned later, instead update the
class/docstring to not claim read-only or add a clear TODO indicating planned
write methods.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f2481c43-9760-470a-aa80-0b815ca47ce8

📥 Commits

Reviewing files that changed from the base of the PR and between 54a20b7 and a6b323a.

📒 Files selected for processing (7)
  • .env.example
  • apps/discord_bot/src/five08/discord_bot/cogs/erpnext.py
  • apps/discord_bot/src/five08/discord_bot/config.py
  • packages/shared/src/five08/clients/erpnext.py
  • packages/shared/src/five08/erpnext_validation.py
  • tests/unit/test_erpnext_client.py
  • tests/unit/test_erpnext_cog.py

Comment thread apps/discord_bot/src/five08/discord_bot/cogs/invoices.py
Comment thread apps/discord_bot/src/five08/discord_bot/cogs/erpnext.py Outdated
Comment thread apps/discord_bot/src/five08/discord_bot/config.py Outdated
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1148844547

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread apps/discord_bot/src/five08/discord_bot/cogs/erpnext.py Outdated
Comment thread apps/discord_bot/src/five08/discord_bot/cogs/erpnext.py Outdated
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 579f81c2eb

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread apps/discord_bot/src/five08/discord_bot/cogs/erpnext.py Outdated
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dbc3e320a4

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread apps/discord_bot/src/five08/discord_bot/cogs/erpnext.py Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (2)
packages/shared/src/five08/clients/erpnext.py (1)

230-245: ⚡ Quick win

Normalize query and bound limit in invoice search.

search_invoices should strip whitespace-only queries and clamp limit to avoid noisy filters and unbounded/invalid page sizes.

Proposed refactor
     def search_invoices(
         self,
         doctype: str,
         query: str = "",
         docstatus: int | None = None,
         limit: int = 10,
     ) -> list[dict[str, Any]]:
         """Search invoices for autocomplete, ordered newest first."""
+        normalized_query = query.strip()
+        normalized_limit = max(1, limit)
         filters: list[Any] = []
-        if query:
-            filters.append([doctype, "name", "like", f"%{query}%"])
+        if normalized_query:
+            filters.append([doctype, "name", "like", f"%{normalized_query}%"])
         if docstatus is not None:
             filters.append([doctype, "docstatus", "=", docstatus])

         params: dict[str, Any] = {
             "fields": json.dumps(["name", "posting_date", "docstatus", "owner"]),
             "order_by": "posting_date desc",
-            "limit_page_length": limit,
+            "limit_page_length": normalized_limit,
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/shared/src/five08/clients/erpnext.py` around lines 230 - 245, In
search_invoices, normalize the incoming query by trimming whitespace and
treating empty/whitespace-only queries as no query (so only append the name
filter if query.strip() is non-empty), and clamp the limit parameter to a safe
range (e.g., coerce to int and bound it between 1 and a sensible max like 100)
before building filters and params; update the logic around the filters list and
the params["limit_page_length"] assignment in the search_invoices function so it
uses the normalized query and clamped limit.
tests/unit/test_erpnext_validation.py (1)

1-6: ⚡ Quick win

Add a contract test for unsupported doctypes.

validate_invoice explicitly raises ValueError for unsupported doctypes, but this behavior is currently untested.

Proposed test addition
 from __future__ import annotations
 
 from typing import Any
+import pytest
 
 from five08.erpnext_validation import validate_invoice
@@
 def test_multiple_issues_reported() -> None:
@@
     result = validate_invoice(invoice, "Purchase Invoice")
     assert len(result.issues) >= 2
+
+
+def test_unsupported_doctype_raises() -> None:
+    with pytest.raises(ValueError, match="Unsupported doctype"):
+        validate_invoice(VALID_SALES_INVOICE, "Delivery Note")

Also applies to: 129-138

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/test_erpnext_validation.py` around lines 1 - 6, Add a contract
test that exercises validate_invoice's error path for unsupported doctypes: call
validate_invoice with a doctype value that is not handled by the function and
assert that it raises a ValueError; place the test in
tests/unit/test_erpnext_validation.py alongside the existing imports and name it
clearly (e.g., test_validate_invoice_unsupported_doctype) so future changes to
validate_invoice will be covered.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/discord_bot/src/five08/discord_bot/cogs/erpnext.py`:
- Around line 148-161: The autocomplete is returning more choices than Discord
allows because search_invoices can return >25 items; before returning from the
function that builds choices (the loop that creates app_commands.Choice from
invoices), truncate the choices list to at most 25 items (e.g., choices =
choices[:25]) or enforce the limit when mapping invoices to choices so only the
first 25 invoices are converted; ensure the truncation happens right before
return to guarantee the response never exceeds Discord's max autocomplete
choices.

In `@packages/shared/src/five08/clients/erpnext.py`:
- Around line 224-225: The current code treats a non-dict payload as "not found"
by returning None from the block that does row = data.get("data"); instead, only
return None for an actual HTTP 404 — for other cases validate that row is a dict
and if it's malformed raise a descriptive error (e.g., ValueError or an
ApiSchemaError) so upstream callers can distinguish schema/bad-response errors
from missing resources; update the code around row = data.get("data") to check
the HTTP status (or response error field) and return None only on 404, otherwise
raise an exception with details about the unexpected payload.

In `@packages/shared/src/five08/erpnext_validation.py`:
- Around line 87-90: The loop over invoice.get("items", []) can still fail when
ERP returns "items": null; before iterating (where item_cost_center and
item_project are used) ensure the items value is a proper list by normalizing it
(e.g., items = invoice.get("items") or [] and/or assert isinstance(items, list)
fallback to []), then iterate over that normalized 'items' variable; update the
loop that creates item_cost_center and item_project to use this guarded 'items'
so a null payload does not raise a TypeError during validation.
- Around line 124-128: The date-parsing try/except around datetime.strptime for
posting_date_raw and due_date_raw only catches ValueError and will still crash
on non-string inputs (TypeError); update the exception handler in the block that
parses posting_date and due_date (the try surrounding datetime.strptime for
posting_date_raw and due_date_raw) to catch both ValueError and TypeError (e.g.,
except (ValueError, TypeError): return) so non-string or bad-format payloads are
handled gracefully.

---

Nitpick comments:
In `@packages/shared/src/five08/clients/erpnext.py`:
- Around line 230-245: In search_invoices, normalize the incoming query by
trimming whitespace and treating empty/whitespace-only queries as no query (so
only append the name filter if query.strip() is non-empty), and clamp the limit
parameter to a safe range (e.g., coerce to int and bound it between 1 and a
sensible max like 100) before building filters and params; update the logic
around the filters list and the params["limit_page_length"] assignment in the
search_invoices function so it uses the normalized query and clamped limit.

In `@tests/unit/test_erpnext_validation.py`:
- Around line 1-6: Add a contract test that exercises validate_invoice's error
path for unsupported doctypes: call validate_invoice with a doctype value that
is not handled by the function and assert that it raises a ValueError; place the
test in tests/unit/test_erpnext_validation.py alongside the existing imports and
name it clearly (e.g., test_validate_invoice_unsupported_doctype) so future
changes to validate_invoice will be covered.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f564b3df-4ceb-4e9d-a578-85929e56bd25

📥 Commits

Reviewing files that changed from the base of the PR and between a6b323a and 8337d0c.

📒 Files selected for processing (6)
  • apps/discord_bot/src/five08/discord_bot/cogs/erpnext.py
  • packages/shared/src/five08/clients/erpnext.py
  • packages/shared/src/five08/erpnext_validation.py
  • tests/unit/test_erpnext_client.py
  • tests/unit/test_erpnext_cog.py
  • tests/unit/test_erpnext_validation.py

Comment thread apps/discord_bot/src/five08/discord_bot/cogs/invoices.py
Comment thread packages/shared/src/five08/clients/erpnext.py Outdated
Comment thread packages/shared/src/five08/erpnext_validation.py Outdated
Comment thread packages/shared/src/five08/erpnext_validation.py
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/shared/src/five08/clients/erpnext.py (1)

669-675: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Validate identifiers and clamp limit in invoice helpers.

Line 669 and Line 687 skip the required strip()/empty checks used by the rest of this client, and Line 716 sends limit without a lower-bound clamp. That can produce malformed resource paths and invalid/zero-length list queries.

Suggested fix
 def get_invoice(self, doctype: str, name: str) -> dict[str, Any] | None:
     """Fetch a single Sales Invoice or Purchase Invoice by name. Returns None on 404."""
+    normalized_doctype = doctype.strip()
+    normalized_name = name.strip()
+    if not normalized_doctype:
+        raise ERPNextAPIError("DocType is required")
+    if not normalized_name:
+        raise ERPNextAPIError(f"{normalized_doctype} name is required")
     try:
         data = self.request(
             "GET",
-            f"/api/resource/{quote(doctype, safe='')}/{quote(name, safe='')}",
+            f"/api/resource/{quote(normalized_doctype, safe='')}/{quote(normalized_name, safe='')}",
         )
@@
 def search_invoices(
     self,
     doctype: str,
@@
 ) -> list[dict[str, Any]]:
@@
+    normalized_doctype = doctype.strip()
+    if not normalized_doctype:
+        raise ERPNextAPIError("DocType is required")
+    normalized_limit = max(1, limit)
     filters: list[Any] = []
     if query:
-        filters.append([doctype, "name", "like", f"%{query}%"])
+        filters.append([normalized_doctype, "name", "like", f"%{query}%"])
     if docstatus is not None:
-        filters.append([doctype, "docstatus", "=", docstatus])
+        filters.append([normalized_doctype, "docstatus", "=", docstatus])
@@
     if owners:
-        or_filters.append([doctype, "owner", "in", owners])
+        or_filters.append([normalized_doctype, "owner", "in", owners])
     if projects:
-        or_filters.append([doctype, "project", "in", projects])
+        or_filters.append([normalized_doctype, "project", "in", projects])
@@
     params: dict[str, Any] = {
         "fields": json.dumps(["name", "posting_date", "docstatus", "owner"]),
         "order_by": "posting_date desc",
-        "limit_page_length": limit,
+        "limit_page_length": normalized_limit,
     }
@@
         "GET",
-        f"/api/resource/{quote(doctype, safe='')}",
+        f"/api/resource/{quote(normalized_doctype, safe='')}",
         params=params,
     )

Also applies to: 713-717

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/shared/src/five08/clients/erpnext.py` around lines 669 - 675, The
get_invoice and the invoice-list helper that builds list queries (e.g.,
get_invoices) currently pass doctype/name and limit straight through; validate
and normalize identifiers by calling .strip() and rejecting/raising or returning
None for empty doctype or name before quoting to avoid malformed resource paths,
and clamp the limit parameter to a sensible lower bound (e.g., max(1,
int(limit))) before including it in query params so you never send zero/negative
limits; update the request calls in get_invoice and the invoice-list helper to
use the stripped values and the clamped limit.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/discord_bot/src/five08/discord_bot/cogs/erpnext.py`:
- Around line 225-227: The autocomplete logic currently assumes invoice "name"
is a string and calls len(inv_name) which raises TypeError for non-string
values; update the checks around inv_name (the variable extracted from each inv
in the autocomplete function/method) to first ensure it's a string (e.g., if not
isinstance(inv_name, str): continue) before calling len or otherwise coerce
safely (inv_name = str(inv_name)) and apply the same safe-type check where the
code later trims or uses inv_name (the block that filters/limits results and
builds the autocomplete response) so non-string names are skipped or handled
without throwing.

---

Outside diff comments:
In `@packages/shared/src/five08/clients/erpnext.py`:
- Around line 669-675: The get_invoice and the invoice-list helper that builds
list queries (e.g., get_invoices) currently pass doctype/name and limit straight
through; validate and normalize identifiers by calling .strip() and
rejecting/raising or returning None for empty doctype or name before quoting to
avoid malformed resource paths, and clamp the limit parameter to a sensible
lower bound (e.g., max(1, int(limit))) before including it in query params so
you never send zero/negative limits; update the request calls in get_invoice and
the invoice-list helper to use the stripped values and the clamped limit.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 79c9741b-20d6-49d6-ae97-cbae3b96ae8d

📥 Commits

Reviewing files that changed from the base of the PR and between eb1373d and fcb4b0b.

📒 Files selected for processing (4)
  • apps/discord_bot/src/five08/discord_bot/cogs/erpnext.py
  • packages/shared/src/five08/clients/erpnext.py
  • tests/unit/test_erpnext_client.py
  • tests/unit/test_erpnext_cog.py

Comment thread apps/discord_bot/src/five08/discord_bot/cogs/erpnext.py Outdated
Fixed a bug where truncating app_commands.Choice.value broke subsequent invoice lookups. Now overlong IDs are skipped entirely, and display names are gracefully truncated with ellipses.
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3f8aa67088

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +209 to +211
include_all, emails, project_ids = await asyncio.to_thread(
self._resolve_access, interaction
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid full dashboard queries inside autocomplete access checks

invoice_name_autocomplete runs _resolve_access on every keystroke, and _resolve_access calls list_dashboard_projects(..., limit=500), which is the heavy dashboard path (it loads full project data/rosters, not just IDs). Since autocomplete interactions must return almost immediately and cannot be deferred, this repeated DB work is on the critical path before the ERPNext HTTP lookup, so users with larger project visibility can hit interaction timeouts and get empty suggestions. Use a lightweight project-id lookup or cache access scope per user to keep autocomplete latency bounded.

Useful? React with 👍 / 👎.

- Rename cogs/erpnext.py → invoices.py, ErpNextCog → InvoicesCog to
  reflect feature scope over system name
- Add include_roster param to list_dashboard_projects (default True,
  backward compatible) and use include_roster=False in _resolve_access
  to skip the roster-members query on every autocomplete keystroke
- Fix get_invoice input validation to raise ERPNextAPIError consistent
  with other ERPNextClient methods
- Clamp search_invoices limit with max(1, limit)
# 1. They have Steering Committee role or above (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"]
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you want we can add Workflows Engineer to this

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the suggestion! Adding Workflows Engineer to _PRIVILEGED_ROLES in a follow-up PR#300.

Copy link
Copy Markdown
Member

@michaelmwu michaelmwu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a Codex generated review. Please validate.

Re-reviewed the invoice permission changes at ff6c09e. The previous blocking feedback is addressed: /validate-invoice now resolves caller access before fetching, unauthorized users without ERP identity are denied before any invoice lookup, direct fetches re-check owner/project access after retrieval, and autocomplete is scoped via owner/project filters before returning choices.

Verification: uv run --all-groups python3 -m pytest tests/unit/test_erpnext_client.py tests/unit/test_invoices_cog.py tests/unit/test_erpnext_validation.py tests/unit/test_projects.py -q passed with 68 tests and 1 existing Discord audioop deprecation warning.

@michaelmwu michaelmwu merged commit e7f288a into 508-dev:main May 21, 2026
7 checks passed
@lairwaves lairwaves deleted the feat/erpnext-invoice-validation branch May 21, 2026 16:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants