Partner Country Executives API
All endpoints require admin authentication (token_admin). The two new permissions used by this module — view-partner-country-executives and manage-partner-country-executives — are wired in but not yet enforced: the route-level pre-handler is gated by ENFORCE_PERMISSIONS = false until the permissions are seeded in the database. Once seeded, flipping the flag re-enables the gate without code changes elsewhere.
Country Executives
List country executives
GET /partner-country-executivesReturns a paginated list of executives for a given role type, hydrated with the administrator profile and configured countries. Designed to feed the DataTable on /partners/country-assignments (PAE and SDR tabs share the endpoint via role_type). Each executive's id is its administrator_id.
Required permission (when enforced): view-partner-country-executives
| Query parameter | Type | Required | Description |
|---|---|---|---|
role_type | 'pae' | 'salesman' | Yes | Filters the table by executive type |
start | number | No | Offset, default 0 |
length | number | No | Page size, default 20 |
sortBy | string | No | One of id, administrator, active, created_at, updated_at |
sortType | string | No | ASC or DESC (defaults to DESC) |
administrator_id | number | No | Exact match on the administrator id |
administrator_name | string | No | LIKE match on users.name |
administrator_email | string | No | LIKE match on users.email |
locale_id | number | No | Returns only executives that have this country configured |
active | 0 | 1 | No | Filter by status |
Response shape:
{
"data": [
{
"id": 12,
"role_type": "pae",
"is_global_fallback": false,
"active": true,
"administrator": {
"id": 482,
"user_id": 1099,
"name": "Jane Doe",
"email": "jane@example.com",
"country": { "code": "MX", "name": "Mexico" }
},
"countries": [
{ "id": 1, "code": "MX", "name": "Mexico" },
{ "id": 5, "code": "CO", "name": "Colombia" }
],
"created_at": "2026-05-30T10:00:00.000Z",
"created_by": { "id": 7, "name": "Admin User" },
"updated_at": "2026-05-30T10:00:00.000Z",
"updated_by": { "id": 7, "name": "Admin User" }
}
],
"recordsTotal": 42,
"recordsFiltered": 42
}Hydration is batch
The library extracts every executive's administrator_id from the page, then runs one SELECT ... WHERE administrator_id IN (...) against asignation_groups_members (filtered by the partner scope and local_id IS NOT NULL) to attach the country list. There is no N+1.
Get one executive
GET /partner-country-executives/{id}Returns a single hydrated executive. Throws 404 if not found.
Required permission (when enforced): view-partner-country-executives
Available administrators (catalog)
GET /partner-country-executives/available-administrators?role_type=paeReturns the administrators that are eligible to be activated for the given role type (i.e. they hold one of the eligible roles defined in ELIGIBLE_ROLES_BY_TYPE) and that do not yet have an assignment for that role type.
Required permission (when enforced): manage-partner-country-executives
Response shape:
{
"data": [
{ "id": 482, "name": "Jane Doe", "role_id": 20, "role_name": "Partners Administrator" }
]
}role_type | Eligible administrator roles |
|---|---|
pae | role_id = 56 (Partners Administrator) |
salesman | role_id = 57 (SDR) |
The eligible-roles list is an array per role type, so a future requirement like "PAEs can also be a manager" is a one-line constant change — no controller or library refactor needed.
Global fallback
GET /partner-country-executives/global-fallback?role_type=paeReturns the active executive that currently holds the is_global_fallback flag for the given role type, or { data: null } if there is none. Used by the UI to surface the "no fallback configured" warning banner.
Required permission (when enforced): view-partner-country-executives
Active executives (dropdown for bulk-assign)
GET /partner-country-executives/active?role_type=paeReturns every active executive for the given role type, regardless of the countries they have configured. The result feeds the dropdown of the bulk-assign offcanvas on the Partners list — the eligible pool is anyone currently active (status_leads = 1) holding an eligible role for that role type.
Required permission (when enforced): view-partner-country-executives
Ordering: the global fallback (if any) is returned first, then the rest by administrator name ASC.
Create an executive
POST /partner-country-executivesRequired permission (when enforced): manage-partner-country-executives
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
role_type | 'pae' | 'salesman' | Yes | |
administrator_id | number | Yes | Must hold an eligible role for the chosen role_type. |
is_global_fallback | boolean | No | Defaults to false. Setting true requires no other active executive currently holds the flag for the same role_type. |
locale_ids | number[] | Yes | At least one country. For PAE, the countries must not already be assigned to another active PAE. |
Pre-write validations (all return 409/422 with a friendly message):
_assertAdministratorEligible— administrator must exist and have an eligible role for the chosenrole_type(role_idinELIGIBLE_ROLES_BY_TYPE)._assertAdministratorAvailable— administrator must not already be an active executive:administrators.status_leadsmust be0and there must be no activeasignation_groups_membersrows for the scope._assertUniqueFallback— only one active global fallback (anasignation_groups_membersrow withlocal_id IS NULL) per scope._assertCountriesAvailable— forrole_typelisted inCOUNTRY_EXCLUSIVE_ROLE_TYPES(today:pae), no requested country may already be assigned to another active executive of the same scope. The conflict response contains adata.conflictsarray with the conflicting country and administrator names so the UI can show "Country X is already owned by Y".
Response: the hydrated row (same shape as the list endpoint), HTTP 201.
Update an executive
PUT /partner-country-executives/{id}Required permission (when enforced): manage-partner-country-executives
Request body: all fields are required (the UI always submits the full state).
| Field | Type | Description |
|---|---|---|
is_global_fallback | boolean | Same uniqueness rule as create, but excludes the current administrator from the conflict check. Toggled via the fallback row (local_id IS NULL). |
active | boolean | Sets administrators.status_leads. Set false to pause the executive without removing their coverage rows. An inactive executive does not occupy the fallback slot. |
locale_ids | number[] | Reconciled with a diff against the existing asignation_groups_members rows: added countries are inserted, removed countries are DELETEd. Each statement is atomic, so a partial failure converges on retry. |
administrator_id and role_type are immutable
A reassignment is intentionally modeled as delete + create, not as a mutation. This keeps the audit log unambiguous and forces an explicit handover decision.
Delete an executive
DELETE /partner-country-executives/{id}Required permission (when enforced): manage-partner-country-executives
Deletes every partner-scope row for the administrator (DELETE FROM asignation_groups_members WHERE administrator_id = ? AND scope = ?) and turns administrators.status_leads off. The 'ticket'-scope rows for that administrator (if any) are untouched.
Guardrails: if the row holds is_global_fallback = 1, the request is rejected with 409 Conflict and a message asking the operator to remove the fallback flag (and reassign it elsewhere if needed) before deleting.
Bulk Assignment from the Partners list
These endpoints live in partner.routes.js but are part of the same feature: they consume administrators.status_leads (the executive registry) as the source of truth for which administrators are eligible to receive partners in bulk.
Bulk-assign a PAE
POST /partners/bulk-assign-administratorUpdates partners.administrator_id to administrator_id for every partner in partner_ids, and writes one partner.pae.assigned log per affected partner.
Required permission: update-partner-administrator
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
partner_ids | number[] (min 1, unique) | Yes | The partners to update |
administrator_id | number | Yes | Must be an active PAE (status_leads = 1 and an eligible PAE role), validated via getActiveByAdministrator. |
Behavior:
- Partners that already have
administrator_idset to the requested value are silently skipped (shouldSkipreturnstrue). - Each successful update writes an audit row in
logswithlog_type = 'partner.pae.assigned', the previous and new administrator id, and the originatingpartner_country_executive_id.
Response:
{
"updated": [101, 102, 103],
"skipped": [104]
}Bulk-assign a Salesman
POST /partners/bulk-assign-salesmanSame shape as the previous endpoint but updates partners.salesman_id for each affected partner and propagates the same salesman to companies.salesman for every row where companies.partner_id = partner.id (i.e. the partner's referred companies). Writes a partner.salesman.assigned log per affected partner that includes the count of referred companies updated (new_value.referrals_updated).
Required permission: update-partner-administrator
Request body: identical to bulk-assign-administrator.
The administrator_id must be an active Salesman (status_leads = 1 and an eligible SDR role). The shared engine _bulkAssignExecutive is reused; only the apply callback (the UPDATE statement and the log payload) differs.
Error responses
| Code | When | Message origin |
|---|---|---|
400 Bad Request | Joi validation failed | failAction returns Boom.badData |
404 Not Found | getById for an unknown executive id, or any partner_id from the bulk endpoints not present in the DB | Boom.notFound |
409 Conflict | Administrator already an active executive (status_leads = 1 or active scope rows), second active global fallback, country already owned by another active PAE, or attempt to delete an executive that holds the global fallback flag | Boom.conflict (with data.conflicts for the country case) |
422 Unprocessable Entity | Administrator does not exist, administrator does not hold an eligible role, or administrator_id is not an active executive when bulk-assigning | Boom.badData |
Permissions Summary
| Permission key | Used by | Access type | Status |
|---|---|---|---|
view-partner-country-executives | GET /partner-country-executives* | Read | Wired but not enforced until the seed runs |
manage-partner-country-executives | POST/PUT/DELETE /partner-country-executives* | Write | Wired but not enforced until the seed runs |
update-partner-administrator | POST /partners/bulk-assign-administrator, POST /partners/bulk-assign-salesman | Write | Enforced today |
