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_idon 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_id→clients.id(ON DELETE CASCADE), part of the composite PK.user_id→users.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 toClientInput/ClientResponse). - Alembic revision:
add_client_salespersons(20260627_add_client_salespersons.py),down_revision = add_bank_tx_settlement_receipt.
What's being implemented¶
- Model: client_salesperson.py.
- Repository:
add_client_salesperson,remove_client_salesperson,get_client_salespersons, andget_clients_by_salespersonin client_repository.py. - Service: matching methods in client_service.py.
- Mutations:
addClientSalesperson/removeClientSalespersonin client_mutations.py. - Queries:
clientSalespersons/clientsBySalespersonin client_queries.py. - Tests: test_client_salespersons.py.
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 withremoveClientSalesperson(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
salespersonstored 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_idon 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.