Skip to content

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:

  1. Country Assignments — a CRUD section under /partners/country-assignments that registers an administrator as an active executive for one or more countries, with an optional global fallback flag.
  2. 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.salesman for rows with companies.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). The role_type is derived from administrators.role_id (a PAE administrator vs. an SDR administrator), so an administrator never needs an explicit role_type column. (The previous dedicated flag allow_partner_referral_assignment is 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_members table, scoped with scope IN ('partner_pae','partner_salesman'). A row with local_id IS NULL marks the global fallback for that scope. The ticket auto-assignment feature uses the same table with scope = '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

ConceptDescription
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 registryadministrators.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_idlocales.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 roleConfigured 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 exclusivityPAE 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-salesmanEndpoints 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

TablePurpose
administratorsHolds 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_membersShared 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_rolesRead for hydration (administrator name/email, country code/name, role description).
partnersTarget of the bulk-assign endpoints. PAE updates partners.administrator_id; Salesman updates partners.salesman_id. Both columns are FK to administrators.id.
companiesRead-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).
logsAudit 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

ColumnTypeNotes
status_leadsBIT(1) / tinyint1 = 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.

ColumnTypeNotes
idint PKAuto-increment
administrator_idint FK → administrators.idThe executive
local_idint FK → locales.idThe country. NULL marks the global-fallback row for the scope
scopevarchar / enum'partner_pae' or 'partner_salesman' for this feature ('ticket' belongs to auto-assignment and is never touched here)
activetinyint1 for active coverage/fallback rows
assignment_typevarcharAlways NULL for partner rows (only meaningful for the ticket scope)
created_byint FK → users.idAudit
created_at / updated_atdatetimeUsed 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

DecisionReasoningAlternatives Considered
Reuse asignation_groups_members (with new partner scopes) instead of dedicated tablesThe 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 itAn 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_leadsA 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 slotA 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 diffEach 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 administratorsThe 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 UIThe 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 SalesmanThe 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 (the status_leads flag + role_id), asignation_groups_members (shared with the ticket auto-assignment feature), users, locales, catalog_administrator_roles, partners, companies, logs (DB).
    • partnerCountryExecutives.constantsROLE_TYPES, ELIGIBLE_ROLES_BY_TYPE, COUNTRY_EXCLUSIVE_ROLE_TYPES, SCOPES, SCOPE_BY_ROLE_TYPE, PARTNER_SCOPES.
    • permissionMiddleware.canByName for route-level permission gates (currently bypassed via ENFORCE_PERMISSIONS).
    • Catalog endpoint GET /catalog/locales (frontend) for the countries dropdown.
  • Shared with: the ticket auto-assignment feature (scope = 'ticket' in asignation_groups_members). The partner flows only read/write scope IN ('partner_pae','partner_salesman'), so the two features stay isolated.
  • External: None.

Envia Admin