ObjPerson manages contact people for notifications, runbooks, and
escalations. A person may be an Axion platform user (linked via the
User field to sys_user) or a purely external contact — a vendor,
on-call engineer, or client representative — with no Axion account.
All notification channel fields are stored on data_person so that
ObjNotify can deliver alerts to any person regardless of whether
they have an Axion login.
Visibility is controlled by the Packages field (comma-separated
list of package names). A person with an empty Packages field is
visible to all packages. All find, search, and list operations filter
by this field automatically.
The module also includes Idnumber — a standalone South African
13-digit ID number validator and parser (candidate for future move
to factory.text).
data_person| Column | Type | Description |
|---|---|---|
PersonGuid |
char(50) | Primary key |
Package |
varchar(255) | Owning package (for data scoping on insert) |
Module |
varchar(255) | Organisational module link |
Name |
varchar(255) | Full name (required) |
Nickname |
varchar(255) | Preferred name or nickname |
User |
varchar(255) | Optional link to sys_user.User |
Email |
varchar(255) | Email address |
Msisdn |
varchar(50) | Mobile number for SMS |
WhatsappNumber |
varchar(50) | WhatsApp number (may differ from Msisdn) |
SlackId |
varchar(100) | Slack user or channel ID |
DiscordId |
varchar(100) | Discord user or channel ID |
PagerdutyId |
varchar(255) | PagerDuty contact ID |
Role |
varchar(100) | e.g. "On-Call Engineer", "Escalation Contact" |
JobTitle |
varchar(255) | Job title |
Companies |
text | Comma-separated list of associated companies |
Packages |
text | Comma-separated list of packages this person is visible to |
City |
varchar(255) | City |
Timezone |
varchar(100) | IANA timezone (e.g. Africa/Johannesburg) |
Notes |
text | Free-text notes |
Active |
char(1) | Y/N soft-delete flag |
LastUpdateDate |
datetime | Set to NOW() on every save() — read-only from app |
LastContactDate |
datetime | Updated via touch_contact() |
Package (singular) is the owning record for data scoping. Packages
(plural) is the authority for visibility: all find/list/search queries
filter using FIND_IN_SET(package, REPLACE(Packages, ' ', '')).
A person with an empty Packages field matches every package.
data_person as a Foreign KeyPersonGuid is referenced as a nullable field in several alert and
incident tables, sitting alongside the existing username-based fields:
| Table | Column(s) |
|---|---|
track_alert |
AcknowledgedByPersonGuid, ResolvedByPersonGuid |
def_alert |
EscalationPersonGuid1/2/3 |
def_alert_incident |
AssignedToPersonGuid, ClosedByPersonGuid |
def_alert_incident_timeline |
PersonGuid |
stage_notify |
PersonGuid |
All columns are nullable — existing string-based fields (AcknowledgedBy,
AssignedTo, etc.) remain in place for backward compatibility.
Inherits from ObjData for database access. Table is created
automatically via create_tables_from_yaml(__file__).
packages_list -> list[str]Returns _Packages parsed into a list, stripping whitespace around
commas. Useful when iterating package associations without manual
split/strip calls.
companies_list -> list[str]Same as packages_list but for _Companies.
read(person_guid) -> boolLoad a person by PersonGuid. Returns True if found, populating
all _* attributes via _load_from_row().
read_by_identifier(identifier, package) -> boolLoad a person by PersonGuid, User, or Name, filtered by package
association. Populates self._* fields like read(). Returns True
if found. When multiple persons share a name, the most recently updated
record wins (ORDER BY LastUpdateDate DESC).
save() -> boolInsert or update the person record (upsert on PersonGuid). Guards
against empty Name. Generates a new PersonGuid automatically if
not set. LastUpdateDate is always written as NOW() server-side.
Returns True on success.
touch_contact(person_guid) -> boolStamps LastContactDate and LastUpdateDate to now. Use after any
communication event involving this person.
delete(person_guid) -> boolSoft-delete by setting Active = 'N'. Returns True on success.
list_persons(package, active_only=True) -> list[dict]Return all persons visible to the package, ordered by Name. Pass
active_only=False to include inactive records.
find_by_identifier(identifier, package) -> dict | NoneFind an active person matching PersonGuid, User, or Name,
filtered by package association. Returns the raw row dict or None.
get_contact(identifier, channel, package) -> strResolve the contact detail for a notification channel. Looks up the
person and returns the channel field. Used by ObjNotify as a fallback
when sys_user has no contact detail for a user.
person = ObjPerson()
msisdn = person.get_contact("john.smith", "SMS")
slack_id = person.get_contact("john.smith", "SLACK")
email = person.get_contact("abc-guid", "EMAIL")
Supported channels: EMAIL, SMS, WHATSAPP, SLACK, DISCORD,
PAGERDUTY. Returns empty string if no match or field is empty.
search(search_term, package) -> list[dict]Search active persons by name, nickname, email, role, company, or
job title — filtered by package association. Returns matches ordered
by name.
ObjNotify.deliver() resolves SMS and WhatsApp contact numbers from
sys_user first. If the user is not found in sys_user, it falls
back to data_person using the get_person_msisdn and
get_person_whatsapp queries added to ObjNotify.yaml.
stage_notify.PersonGuid holds the data_person reference when a
notification is targeted directly at a person rather than an Axion
user account.
Persons are referenced in alert definitions via EscalationPersonGuid1,
EscalationPersonGuid2, and EscalationPersonGuid3 on def_alert
(alongside the existing EscalationLevel1/2/3 varchar fields). When
an alert escalates, the escalation workflow can resolve contact details
via ObjPerson.get_contact().
The AcknowledgedByPersonGuid and ResolvedByPersonGuid columns on
track_alert record which person handled each trigger instance.
create_incident(…, assigned_to_person_guid) — links the assignedAssignedToPersonGuid.close_incident(…, person_guid) — records the closing person viaClosedByPersonGuid and passes it through to the timeline entry.add_incident_timeline(…, person_guid) — records PersonGuid onStandalone validator for South African 13-digit ID numbers.
v = Idnumber("8001015009087")
v.is_valid() # True
v.gender() # 'M'
v.is_citizen() # True
v.find("Reference: 8001015009087 submitted") # '8001015009087'
| Method | Description |
|---|---|
is_valid(id_number) |
Luhn-based check; returns bool |
gender() |
'M' or 'F' from digits 7–10 |
is_citizen() |
True if digit 10 is 0 (SA citizen) |
find(text) |
Extract first valid ID from a text string |
Legacy PascalCase aliases (IsValid, Gender, Find, SetId) are
kept for backward compatibility.
# Validate a South African ID number
python factory.core/ObjPerson.py validate-id 8001015009087
# List contact persons visible to the active package
python factory.core/ObjPerson.py list-people [--package PKG] [--all]
# Show full details for one person
python factory.core/ObjPerson.py show <PersonGuid>
# Add a new contact person
python factory.core/ObjPerson.py add "Jane Smith" \
--nickname "Jane" \
--job-title "Senior Engineer" \
--role "On-Call Engineer" \
--email jane@example.com \
--msisdn +27821234567 \
--whatsapp +27821234567 \
--slack-id U01ABCDE \
--discord-id 123456789 \
--pagerduty-id PXXXXX \
--companies "Acme Corp" \
--packages "homechoice,fullhouse" \
--city "Cape Town" \
--timezone "Africa/Johannesburg" \
--user jsmith # optional: links to sys_user
--package homechoice
# Stamp LastContactDate to now
python factory.core/ObjPerson.py touch-contact <PersonGuid>
ObjNotify — notification delivery; uses data_personObjAlert — alert engine; escalation targets referencePersonGuid on def_alertObjAlertIncident — incident lifecycle;AssignedToPersonGuid, ClosedByPersonGuid, and timeline PersonGuiddata_person