Skip to content

Client Default Salespersons

Date: 2026-06-27

Problem / motivation

Sales documents (quotes, orders, invoices) already carry a single salesperson_id, but there was no way to record which salesperson(s) "own" a client. Agencies want to assign one or more default salespersons to a client so the frontend can surface them on the client record and build a "my clients" view for a given salesperson.

In scope

  • A many-to-many link between clients and salespersons (users).
  • Dedicated mutations to add / remove a default salesperson on a client — deliberately kept out of the client create/update input.
  • Dedicated queries to read a client's default salespersons and to list the clients a given user is a default salesperson for — kept off the client response type.

Out of scope

  • Managing default salespersons through ClientInput / ClientResponse. The link is managed and read through its own mutations/queries.
  • Auto-prefilling salesperson_id on new quotes/orders/invoices from the client default. The frontend reads the defaults and decides what to do. (See Future additions.)
  • Commission/role validation of the assigned users — any user may be linked.

Data model changes

  • New table client_salespersons — association row for the Client ⇄ default-salesperson (User) many-to-many.
  • client_idclients.id (ON DELETE CASCADE), part of the composite PK.
  • user_idusers.id (ON DELETE CASCADE), part of the composite PK.
  • Model: ClientSalesperson.
  • Association rows are managed explicitly in the repository (no relationship on Client, and no change to ClientInput / ClientResponse).
  • Alembic revision: add_client_salespersons (20260627_add_client_salespersons.py), down_revision = add_bank_tx_settlement_receipt.

What's being implemented

ClientInput and ClientResponse are intentionally untouched.

GraphQL surface

Kind Field Notes
Mutation addClientSalesperson(clientId: UUID!, userId: UUID!): [User!]! Links a user; idempotent. Returns the client's full default-salesperson list.
Mutation removeClientSalesperson(clientId: UUID!, userId: UUID!): [User!]! Unlinks a user. Returns the remaining default-salesperson list.
Query clientSalespersons(clientId: UUID!): [User!]! The users set as defaults for the client.
Query clientsBySalesperson(userId: UUID!): [Client!]! Clients for whom the user is a default salesperson.

All four are guarded by the CLIENTS path permission.

RBAC

No new Path/Resource. The link is part of the existing CLIENTS resource; the new query reuses PathPermissionAccess(Path.CLIENTS).

Frontend contract

  • Managing a client's default salespersons is separate from the client create/update form. Add one with addClientSalesperson(clientId, userId) and remove one with removeClientSalesperson(clientId, userId); both return the client's resulting list of default salespersons (User[]) so the UI can refresh in place. Add is idempotent.
  • Read a client's defaults with clientSalespersons(clientId) (not a field on the client object).
  • A salesperson "my clients" view can call clientsBySalesperson(userId).
  • These are defaults for display/pre-fill only — they do not change the salesperson stored on any quote/order/invoice unless the frontend sets it.

What shipped

Everything in scope above. task all is green and the repository tests pass.

Future additions

  • Auto-prefill salesperson_id on new quotes/orders/invoices from the client default. Deferred: the desired behavior with multiple defaults (pick one? require a choice?) needs product input before wiring it server-side.