Skip to content

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-executives

Returns 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 parameterTypeRequiredDescription
role_type'pae' | 'salesman'YesFilters the table by executive type
startnumberNoOffset, default 0
lengthnumberNoPage size, default 20
sortBystringNoOne of id, administrator, active, created_at, updated_at
sortTypestringNoASC or DESC (defaults to DESC)
administrator_idnumberNoExact match on the administrator id
administrator_namestringNoLIKE match on users.name
administrator_emailstringNoLIKE match on users.email
locale_idnumberNoReturns only executives that have this country configured
active0 | 1NoFilter by status

Response shape:

json
{
    "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=pae

Returns 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:

json
{
    "data": [
        { "id": 482, "name": "Jane Doe", "role_id": 20, "role_name": "Partners Administrator" }
    ]
}
role_typeEligible administrator roles
paerole_id = 56 (Partners Administrator)
salesmanrole_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=pae

Returns 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=pae

Returns 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-executives

Required permission (when enforced): manage-partner-country-executives

Request body:

FieldTypeRequiredDescription
role_type'pae' | 'salesman'Yes
administrator_idnumberYesMust hold an eligible role for the chosen role_type.
is_global_fallbackbooleanNoDefaults to false. Setting true requires no other active executive currently holds the flag for the same role_type.
locale_idsnumber[]YesAt 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):

  1. _assertAdministratorEligible — administrator must exist and have an eligible role for the chosen role_type (role_id in ELIGIBLE_ROLES_BY_TYPE).
  2. _assertAdministratorAvailable — administrator must not already be an active executive: administrators.status_leads must be 0 and there must be no active asignation_groups_members rows for the scope.
  3. _assertUniqueFallback — only one active global fallback (an asignation_groups_members row with local_id IS NULL) per scope.
  4. _assertCountriesAvailable — for role_type listed in COUNTRY_EXCLUSIVE_ROLE_TYPES (today: pae), no requested country may already be assigned to another active executive of the same scope. The conflict response contains a data.conflicts array 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).

FieldTypeDescription
is_global_fallbackbooleanSame uniqueness rule as create, but excludes the current administrator from the conflict check. Toggled via the fallback row (local_id IS NULL).
activebooleanSets administrators.status_leads. Set false to pause the executive without removing their coverage rows. An inactive executive does not occupy the fallback slot.
locale_idsnumber[]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-administrator

Updates 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:

FieldTypeRequiredDescription
partner_idsnumber[] (min 1, unique)YesThe partners to update
administrator_idnumberYesMust be an active PAE (status_leads = 1 and an eligible PAE role), validated via getActiveByAdministrator.

Behavior:

  • Partners that already have administrator_id set to the requested value are silently skipped (shouldSkip returns true).
  • Each successful update writes an audit row in logs with log_type = 'partner.pae.assigned', the previous and new administrator id, and the originating partner_country_executive_id.

Response:

json
{
    "updated": [101, 102, 103],
    "skipped": [104]
}

Bulk-assign a Salesman

POST /partners/bulk-assign-salesman

Same 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

CodeWhenMessage origin
400 Bad RequestJoi validation failedfailAction returns Boom.badData
404 Not FoundgetById for an unknown executive id, or any partner_id from the bulk endpoints not present in the DBBoom.notFound
409 ConflictAdministrator 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 flagBoom.conflict (with data.conflicts for the country case)
422 Unprocessable EntityAdministrator does not exist, administrator does not hold an eligible role, or administrator_id is not an active executive when bulk-assigningBoom.badData

Permissions Summary

Permission keyUsed byAccess typeStatus
view-partner-country-executivesGET /partner-country-executives*ReadWired but not enforced until the seed runs
manage-partner-country-executivesPOST/PUT/DELETE /partner-country-executives*WriteWired but not enforced until the seed runs
update-partner-administratorPOST /partners/bulk-assign-administrator, POST /partners/bulk-assign-salesmanWriteEnforced today

Envia Admin