Skip to main content

Part 1 β€” Ontology & Taxonomy Admin

One of eight part-DRs decomposing the Notify-Me feature (CU-86c1p963g / Track B). Siblings: tagging, resident prefs, engine/selector, rollout/flags. Supersedes the taxonomy sections of the earlier single-file v1 DR (now removed).

TL;DR​

A single controlled vocabulary β€” Gali's "City Events Ontology" v2 (city-events-ontology.json) β€” is the source of truth for how every event is classified and what a resident can subscribe to. Ten categories, each with a 3-letter code and a colour, each holding a two-level tree (concept group β†’ applied tag). It is greenfield: no migration from the four legacy category systems (system-categories, communities-categories, community_tags, tag_groups), which are flat/no-alias and superseded. Super-admins own the canonical L1/L2; org/community admins add aliases (display-only, multi-language). L3 (user-created) is a later phase.

Data model ()​

Mongo collection notify-categories (apps/notifications/src/notify-categories/schemas/notify-category.schema.ts). Flat rows + parentKey form the tree (read = one find, build tree in memory, cache β€” no $graphLookup):

fieldmeaning
keystable, language-independent, dotted path e.g. mai.sports_and_fitness.team_sports
typeone of the 10 category codes (NotifyCategoryTypeEnum)
code, color, multidenormalised category facet (code MAI…GEN, hex colour, cardinality)
level0 = category, 1 = concept group, 2 = applied tag
parentKeyparent row's key (undefined at level 0)
labelsa lang->text map (English default) for display
scopesystem | org | community
sourcecanonical | requested (the request flow)
statusactive | pending | rejected
overrides[]per-scope aliases { scope, scopeId, labels{he,en} }

The ontology JSON lives in-repo at apps/notifications/src/notify-categories/ontology/city-events-ontology.ts; the seed (notify-categories.seed.ts) flattens it to the tree on module init (idempotent $setOnInsert bulkWrite). Seeded shape: 282 rows (10 categories, 60 groups, 212 tags).

Aliases (org/community relabel)​

An alias is a display label override only β€” the key never changes, so matching/analytics are unaffected. Stored as overrides[] embedded in each row (cache-resolved, no runtime lookup β€” the argument that satisfied the no-lookup requirement). Precedence at read time: community > org > canonical. Aliases are a multi-language lang->text map (English default; add/remove any supported language) β€” fixing the single-language gap.

API ( β€” internal-gateway, @SuperGuard)​

apps/internal-gateway/src/controllers/notify-digest/notify-digest.controller.ts, mounted under /api/v2/notify:

  • GET /categories β€” full taxonomy (282 rows).
  • POST /categories β€” create a node (CreateNotifyCategoryDto, now carries code/color/multi).
  • PUT /categories/:key β€” update label/status/emoji.
  • DELETE /categories/:key β€” delete, cascades to descendant keys.
  • PUT /categories/:key/override β€” upsert a per-scope multi-language alias.

RPC patterns on the notifications service: NotifyDigestEndpointsEnum (GET/CREATE/UPDATE/DELETE_NOTIFY_CATEGORY, UPSERT_NOTIFY_CATEGORY_OVERRIDE).

Surfaces​

  • Super-admin taxonomy admin (support-tool /notify-digest): categoryβ†’groupβ†’tag tree with colours/codes, collapse + filter, per-node alias (multi-language modal) + delete (cascade). From @bewith-dev/design-system (Button/TextField/Typography/Search) + MUI layout primitives.
  • Org-dashboard "Notifications Categories" (Settings subsection, planned): view the canonical taxonomy read-only, manage org/community aliases only. Mirror batch-settings.tsx + CommunitiesConfigurationStore (MobX + DS).

Decisions locked​

  1. Greenfield, no migration; the 4 legacy systems are superseded.
  2. L1+L2 = system canonical (super-admin); org/community give aliases; L3 (user-created) = next phase.
  3. Aliases are display-only, multi-language, precedence community > org > canonical, cache-resolved.
  4. Key is UUID-like stable dotted path, language-independent; labels are separate/deferrable.

Open questions​

  • OQ1 (Gali): the district/department values in the JSON are placeholders β€” replace with each city's real taxonomy before launch; does it map 1:1 onto existing Bewith data?
  • Request flow (status: pending + a resident/admin "request a category" action) β€” UX undefined; a later phase.
  • Versioning of the ontology (catalogVersion) + an admin approval workflow (FR-A6/A7) β€” P1/P2.

Design-system gap​

The DS has no tree/accordion/card/table, and its Chip is option-typed (ObjectOptionType, not a plain label). The admin tree therefore uses MUI layout primitives + DS-token values. Follow-up: add a plain label-Chip variant and a tree/accordion to the DS (via /add-component) so future taxonomy UIs are fully DS-native.

Testing​

  • Seed idempotency: re-run leaves 282 rows, no dupes.
  • Cascade delete: deleting a category removes its groups + tags.
  • Alias upsert: replaces the same (scope, scopeId), persists the label map.