Skip to content

[Bug] UNCATEGORIZED placeholder customer leaks into the customer list and isn't delete-protected #3

Description

@sjohnston1972

Background

When a brief is uploaded, intake tries to determine the customer. If neither the LLM nor the Customer: regex yields an id, the background finisher assigns the brief to a fallback customer literally named "UNCATEGORIZED" and creates that customer row:

  • backend/app/api/intake.py:146new_cust = (parsed.get("customer_id") or "").strip() or "UNCATEGORIZED"
  • backend/app/api/intake.py:214 — same fallback in the upload path.

There is a separate synthetic bucket, _PENDING, used while an upload is still resolving.

Problem / Goal

The customer list and the delete-protection set only account for _PENDING, not UNCATEGORIZED:

  • backend/app/api/customers.py:30 filters the listing with where(Customer.id != "_PENDING") — so UNCATEGORIZED does show up in the customer list as if it were a real customer.
  • backend/app/api/customers.py:19SYSTEM_CUSTOMERS = {"LEGACY", "_PENDING"}UNCATEGORIZED is not protected, so it's user-deletable even though the system recreates it on the next unresolved upload.

Result: a synthetic bucket masquerades as a customer, silently accumulates every brief the extractor couldn't attribute, and can be deleted only to reappear. This is confusing and makes it easy to lose track of unattributed briefs.

Problem / Goal (intended behavior)

Treat UNCATEGORIZED as a system-managed bucket, consistently, everywhere _PENDING and LEGACY are handled — OR surface it deliberately and distinctly (a labeled "Unattributed" group) rather than as a normal customer. Pick one and apply it consistently.

Where to look

  • backend/app/api/intake.py:146, 214 — where UNCATEGORIZED is minted.
  • backend/app/api/customers.py:19, 30, 61SYSTEM_CUSTOMERS, the listing filter, and the delete guard.
  • Define the fallback id once as a named constant (like PENDING_CUSTOMER_ID at intake.py:44) instead of a bare string literal in two places.

Suggested approach

  1. Introduce a shared constant UNCATEGORIZED_CUSTOMER_ID = "UNCATEGORIZED" (co-locate with PENDING_CUSTOMER_ID) and use it at both intake call sites.
  2. Decide the product behavior:
    • Option A (hide like _PENDING): add it to the listing exclusion and to SYSTEM_CUSTOMERS. Then add a dedicated way to review unattributed briefs (e.g. they already show under the customer detail view) so they aren't lost.
    • Option B (surface distinctly): keep it visible but add it to SYSTEM_CUSTOMERS so it can't be deleted, and label it clearly in the UI as the unattributed bucket.
  3. Whichever option: it must not be freely deletable-then-auto-recreated.

Acceptance criteria

  • The fallback customer id is a single named constant, not a repeated literal.
  • UNCATEGORIZED cannot be deleted via DELETE /api/customers/{id} (returns the same 409 as _PENDING/LEGACY), OR the product deliberately allows it and the UI reflects that — decided and documented in the PR.
  • Its treatment in the customer listing is intentional and consistent with the chosen option.
  • Unattributed briefs remain discoverable.

Testing

Local backend (cd backend && uvicorn app.main:app --reload --port 8080):

  1. POST /api/intake/text with text that has no Customer: line and nothing the LLM can attribute; wait for status=ready.
  2. GET /api/customers — confirm UNCATEGORIZED is handled per the chosen option (hidden, or clearly labeled).
  3. DELETE /api/customers/UNCATEGORIZED — confirm it's refused (Option A/B both protect it).

Out of scope

  • Reworking the whole customer-attribution flow or the _PENDING handoff.
  • Frontend visual design beyond the minimal label needed for the chosen option.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions