Partner Country Executives
Per-country assignment of PAE (Partner Account Executive) and Salesman / SDR administrators, plus bulk assignment of those executives to partner companies.
Overview
The module solves a single business need: for any country (locale), the platform must know which administrator owns the partner relationship for that country, both as the day-to-day account executive (PAE) and as the sales development representative (Salesman / SDR). The same need is met by two cooperating capabilities:
- Country Assignments — a CRUD section under
/partners/country-assignmentsthat registers an administrator as an active executive for one or more countries, with an optional global fallback flag. - Bulk assignment from the Partners list — a multi-select action on the existing partners table that reassigns the PAE (
partners.administrator_id) or Salesman (partners.salesman_id) of many partners at once, validated against the active executives configured in step 1. The Salesman flow also propagates the new salesman to every company referred by each affected partner (companies.salesmanfor rows withcompanies.partner_id = partner.id), so referrals always inherit the salesman of the partner that referred them.
Storage model
This module does not own dedicated tables. It reuses two existing structures:
- Executive registry — the boolean column
administrators.status_leads(1= active executive). This column is shared with the leads-assignment feature, so for PAE/SDR roles it does double duty (receives leads + is an active partner executive). Therole_typeis derived fromadministrators.role_id(a PAE administrator vs. an SDR administrator), so an administrator never needs an explicitrole_typecolumn. (The previous dedicated flagallow_partner_referral_assignmentis deprecated and no longer read/written; it remains in the table pending a later migration.) - Country coverage + global fallback — rows in the shared
asignation_groups_memberstable, scoped withscope IN ('partner_pae','partner_salesman'). A row withlocal_id IS NULLmarks the global fallback for that scope. The ticket auto-assignment feature uses the same table withscope = 'ticket'and is never touched by the partner flows.
Because an administrator has a single role, the executive id exposed to the API is simply the administrator_id — it is unique within a role_type. The API response shape is preserved from the original dedicated-table implementation, so the frontend was unaffected by the storage change.
Why reuse asignation_groups_members instead of new tables
The ticket auto-assignment feature already models "administrator covers these countries" with exactly the columns this feature needs (administrator_id, local_id, active, scope). Adding a scope value (partner_pae / partner_salesman) reuses that battle-tested structure and its hydration patterns instead of introducing — and migrating — two new tables. The scope column keeps the partner rows fully isolated from the ticket rows.
Key Concepts
| Concept | Description |
|---|---|
role_type | 'pae' or 'salesman'. There is no role_type column: it is derived from administrators.role_id via ROLE_TYPE_BY_ROLE_ID, and mapped to a storage scope via SCOPE_BY_ROLE_TYPE. |
scope | 'partner_pae' or 'partner_salesman' — the value written to asignation_groups_members.scope so partner rows never collide with the 'ticket' scope. Defined in SCOPES / PARTNER_SCOPES. |
| Executive registry | administrators.status_leads = 1. Toggled when an executive is activated/paused. Shared with the leads-assignment feature (same column). |
Country (local_id) | Each executive has 0..N countries as asignation_groups_members rows (local_id → locales.id). The country list is hydrated in a single batch query to avoid N+1. |
Global fallback (is_global_fallback) | Represented by an asignation_groups_members row with local_id IS NULL. At most one active executive per scope can hold it. Acts as the catch-all owner when a partner does not match any specific country assignment. |
| Eligible role | Configured per role type in ELIGIBLE_ROLES_BY_TYPE: PAE = administrator role 56, Salesman = administrator role 57 (SDR). Enforced at the application layer in _assertAdministratorEligible. |
| Country exclusivity | PAE is listed in COUNTRY_EXCLUSIVE_ROLE_TYPES: a country can be owned by at most one active PAE. Salesman is not exclusive — multiple SDRs can share a country (pool model). |
bulk-assign-administrator / bulk-assign-salesman | Endpoints that reuse administrators.status_leads as the source of truth for which administrators are eligible to receive partners in bulk. |
Data Flow
Activate an executive (PAE or Salesman)
Bulk-assign an executive to many partners
The Partners table emits a multi-select event; the offcanvas resolves the active executive (using getActiveByAdministrator) before applying any update. Per-partner writes are wrapped in a shared engine (_bulkAssignExecutive) that delegates the actual UPDATE to a role-specific apply() callback and emits an audit log per affected partner.
Database
The feature introduces no new tables. It reuses one column on administrators and the shared asignation_groups_members table (also used by the ticket auto-assignment feature under scope = 'ticket').
Tables
| Table | Purpose |
|---|---|
administrators | Holds the executive registry flag status_leads and the role_id that derives the role_type. Also read for hydration (name/email/role + country). |
asignation_groups_members | Shared table. Stores the country coverage and the global fallback for the partner scopes (partner_pae, partner_salesman). One row per (administrator_id, local_id, scope); a row with local_id IS NULL is the global fallback. |
users / locales / catalog_administrator_roles | Read for hydration (administrator name/email, country code/name, role description). |
partners | Target of the bulk-assign endpoints. PAE updates partners.administrator_id; Salesman updates partners.salesman_id. Both columns are FK to administrators.id. |
companies | Read-only for PAE; write target for the Salesman propagation step. The new salesman is also written to companies.salesman for every row where companies.partner_id = partner.id (the partner's referrals). |
logs | Audit row written per affected partner during a bulk operation (partner.pae.assigned, partner.salesman.assigned). The Salesman log records referrals_updated so it is clear how many referred companies inherited the new salesman. |
Column — administrators.status_leads
| Column | Type | Notes |
|---|---|---|
status_leads | BIT(1) / tinyint | 1 = the administrator is an active country executive. The role_type is derived from administrators.role_id (56 = PAE, 57 = SDR). This column is shared with the leads-assignment feature: it is editable from the administrator table/modal (permission administrators-edit-leads) and from the Country Assignments section, and both write the same value. To avoid side effects, salesman.util.js MDRON queries exclude roles 56/57. |
Because this is a single boolean per administrator and the role_type comes from the role, an administrator can be only one executive type at a time. The pre-check _assertAdministratorAvailable rejects activating an administrator whose status_leads is already on (or who already has active rows for the scope).
Shared table — asignation_groups_members (partner scopes)
Only the columns relevant to the partner scopes are listed; the table carries additional columns (ticket_type_id, carrier_id, service_id, …) used exclusively by the 'ticket' scope.
| Column | Type | Notes |
|---|---|---|
id | int PK | Auto-increment |
administrator_id | int FK → administrators.id | The executive |
local_id | int FK → locales.id | The country. NULL marks the global-fallback row for the scope |
scope | varchar / enum | 'partner_pae' or 'partner_salesman' for this feature ('ticket' belongs to auto-assignment and is never touched here) |
active | tinyint | 1 for active coverage/fallback rows |
assignment_type | varchar | Always NULL for partner rows (only meaningful for the ticket scope) |
created_by | int FK → users.id | Audit |
created_at / updated_at | datetime | Used to derive the executive's created_at (MIN) and updated_at (MAX) in the list hydration |
Country reconciliation on update() diffs the current vs. target local_ids: added countries are inserted via insertBatch, removed ones are DELETEd. The fallback row is reconciled the same way (_syncFallback). Removing an executive deletes every partner-scope row for that administrator and turns the flag off.
No per-executive created_by / updated_by
The previous dedicated tables tracked who created/updated each executive. In the shared-table model those audit fields are not tracked per executive, so the API returns created_by / updated_by as null to preserve the response shape.
Entity relationships
Key Decisions
| Decision | Reasoning | Alternatives Considered |
|---|---|---|
Reuse asignation_groups_members (with new partner scopes) instead of dedicated tables | The ticket auto-assignment feature already models "administrator covers these countries". Adding scope = 'partner_pae' / 'partner_salesman' reuses that structure and avoids creating + migrating two new tables. | Dedicated partner_country_executives + partner_country_executive_locales tables — the original design, replaced: it duplicated a structure that already existed. |
Derive role_type from administrators.role_id instead of storing it | An administrator has a single role, so the role already determines whether they are a PAE or an SDR executive. A separate role_type column would be redundant and could drift from the role. | Storing role_type per executive row — rejected: redundant and a source of inconsistency. Consequence: an administrator can only be one executive type at a time. |
Executive registry is the boolean administrators.status_leads | A single flag per administrator is enough to mark "active executive"; combined with the role it fully identifies the executive. The bulk-assign endpoints read the same flag as their source of truth. | A dedicated column allow_partner_referral_assignment — the previous design, replaced: reusing the existing status_leads avoids maintaining a redundant flag, at the cost of coupling with the leads feature (mitigated by excluding roles 56/57 from the MDRON queries). A status row in asignation_groups_members — rejected: would require a synthetic "header" row with no country. |
Country exclusivity enforced in the application layer (_assertCountriesAvailable) | MySQL cannot express a UNIQUE constraint scoped to a subset of rows (only scope='partner_pae', only active=1). | DB triggers — rejected: harder to reason about than a single library validator. |
Global fallback uniqueness enforced in the application layer (_assertUniqueFallback) | The constraint depends on scope, active and local_id IS NULL, not on a flat unique index. | Partial index — not portable across the MySQL versions used in production. |
| Inactive executives do not occupy the fallback slot | A paused PAE is not effectively receiving partners, so it should not block setting a new fallback. | Treating any fallback row as occupying the slot — rejected: would force operators to delete rows just to swap the fallback. |
administrator_id / role_type are immutable on update (id === administrator_id) | A reassignment is a different identity, not a mutation. Forcing a delete + create avoids ambiguity in audit logs and forces explicit handover. | Allowing the administrator to change in PUT — rejected: makes the audit trail confusing. |
Idempotent country / fallback reconciliation (_syncLocales, _syncFallback) using a diff | Each INSERT/DELETE statement is atomic, so a partial failure converges on retry. | Wipe-and-replace — rejected: it would briefly leave an executive with zero countries, breaking concurrent reads. |
Bulk-assign uses administrators.status_leads as the source of eligible administrators | The flag is exactly the pool of administrators allowed to receive partners. Reusing it eliminates the risk of assigning a partner to an admin that is not configured here. | Allowing any administrator with the right role — rejected: bypasses the "is an active executive" semantics. |
Permission middleware temporarily disabled (ENFORCE_PERMISSIONS = false) on routes and UI | The new view-partner-country-executives and manage-partner-country-executives permissions are not yet seeded in the database; enforcing them right now would 403 every request. | Hard-coding the existing partner permissions — rejected: would expose the section to roles that should not see it long-term. The flag is a one-line flip once the seeds run in production. |
BulkAssignExecutiveOffcanvas is shared across PAE and Salesman | The flow is identical apart from the endpoint name. A single component keeps the UX consistent. | One component per role — rejected: 95% duplication. |
Dependencies
- Internal:
administrators(thestatus_leadsflag +role_id),asignation_groups_members(shared with the ticket auto-assignment feature),users,locales,catalog_administrator_roles,partners,companies,logs(DB).partnerCountryExecutives.constants—ROLE_TYPES,ELIGIBLE_ROLES_BY_TYPE,COUNTRY_EXCLUSIVE_ROLE_TYPES,SCOPES,SCOPE_BY_ROLE_TYPE,PARTNER_SCOPES.permissionMiddleware.canByNamefor route-level permission gates (currently bypassed viaENFORCE_PERMISSIONS).- Catalog endpoint
GET /catalog/locales(frontend) for the countries dropdown.
- Shared with: the ticket auto-assignment feature (
scope = 'ticket'inasignation_groups_members). The partner flows only read/writescope IN ('partner_pae','partner_salesman'), so the two features stay isolated. - External: None.
Related Documentation
- User Guide: Partner Country Executives Guide — End-user instructions for activating PAE / SDR executives and using the bulk-assign action.
- API Endpoints: Partner Country Executives API — Endpoint reference for developers.
- UI Screens: Partner Country Executives UI — Frontend file tree, component responsibilities, and permission gates.
