Skip to main content

Technical Documentation

Audience: Backend developers, system architects, AI/code-generation tools
Scope: Algorithm, error handling, integration table, and testing specification


1. Hierarchy Algorithm (PostgreSQL Ltree)

1.1 Path Format

  • Strategy: PostgreSQL Ltree Extension
  • Path segments: UUID-based identifiers (typically displayed as numeric segments for readability)
  • Separator: .
  • Index: GiST index on path_ltree column for fast hierarchical queries

Example:

uuid_a.uuid_b.uuid_c

Displayed as:

0001.0002.0003

1.2 Create Algorithm

Case A – Create Root Node

  1. Create organization unit with parent_id = NULL
  2. Database trigger or application logic generates path_ltree (typically starts with first segment)
  3. Persist new unit with generated path

Case B – Create Child Node

  1. Validate parent exists and is active
  2. Validate type hierarchy (child type level > parent type level)
  3. Create organization unit with parent_id set
  4. Database trigger or application logic generates path_ltree by concatenating parent path + new UUID segment
  5. Persist child with generated path

Note: Path generation is typically handled by database triggers or Prisma middleware. Application code primarily handles validation and business logic.


1.3 Update Parent (Move Algorithm)

Intent: Change parent organization unit and update path

Steps:

  1. Validate new parent is not descendant of current node (using ltree path check)
  2. Validate type compatibility and active status
  3. Validate hierarchy (new parent type level < current type level)
  4. Update parent_id field
  5. Database trigger or application logic recalculates path_ltree for current unit and all descendants
  6. Persist updates in a single transaction

Circular Reference Check:

// Check if newParent.pathLtree starts with currentUnit.pathLtree
if (newParent.pathLtree.startsWith(currentUnit.pathLtree)) {
throw new BadRequestException({
message: 'Cannot set parent to a descendant organization unit',
reason: 'organization-unit.circular-reference-descendant',
});
}

Rollback strategy:

  • Any failure triggers full transaction rollback

2. Error Handling & Error Codes

2.1 Error Response Shape

{
"success": false,
"statusCode": 400,
"message": "Organization unit type level must be higher than parent type level",
"reason": "organization-unit.type-hierarchy-invalid",
"details": {
"parentTypeLevel": 2,
"currentTypeLevel": 1
},
"path": "/api/v1/organization-units",
"timestamp": "2025-12-19T14:30:00+07:00"
}

2.2 Standard Error Codes

CodeHTTPDescription
organization-unit.not-found404Organization unit not found
organization-unit.parent-not-found404Parent organization unit not found
organization-unit.parent-inactive400Parent organization unit is inactive
organization-unit.type-not-found404Organization unit type not found
organization-unit.type-hierarchy-invalid400Type level must be higher than parent type level
organization-unit.circular-reference-self400Organization unit cannot be its own parent
organization-unit.circular-reference-descendant400Cannot set parent to a descendant organization unit
organization-unit.has-active-children400Cannot delete organization unit with active children
organization-unit.already-inactive400Organization unit is already inactive
organization-unit.not-soft-deleted400Organization unit must be soft deleted first
organization-unit.has-children400Cannot hard delete organization unit with children

3. Event Model

Note: Event model is not currently implemented. This section is reserved for future event-driven architecture.

3.1 Proposed Events (Future Implementation)

Event NameTrigger
organization_unit.createdAfter successful creation
organization_unit.updatedAttribute update
organization_unit.movedParent or path change
organization_unit.activatedis_active = true
organization_unit.deactivatedis_active = false
organization_unit.deletedSoft delete
organization_unit.hard_deletedHard delete

3.2 Proposed Event Payload Examples

{
"event": "organization_unit.moved",
"payload": {
"id": "uuid",
"old_path_ltree": "0001.0002",
"new_path_ltree": "0001.0003",
"old_parent_id": "uuid-old-parent",
"new_parent_id": "uuid-new-parent",
"tenant_id": "uuid",
"occurred_at": "ISO8601"
}
}

Events MUST be published after transaction commit.


4. Integration Table (Cross-Module Usage)

ModuleIntegration PurposeDependency Type
EmployeesAssign owners to unitsForeign key
Core ModulesModule availabilityMany-to-many
TagsCategorization tagsMany-to-many
DocumentsFile attachmentsOne-to-many
Company BanksAssociated bank accountsMany-to-many
AuthorizationScope & access controlRead-only
ReportingAggregation by hierarchyRead-only

5. Auto-Tag Creation Feature

5.1 Behavior

When creating an organization unit, the system automatically creates tags from:

  1. name - Organization unit name
  2. short_name - Short name (if different from name)
  3. code - Organization unit code (if provided)
  4. type_key - Organization unit type key

5.2 Algorithm

// Pseudo-code
const autoTagsToCreate = [];

if (dto.name) autoTagsToCreate.push(dto.name);
if (dto.short_name && dto.short_name !== dto.name) {
autoTagsToCreate.push(dto.short_name);
}
if (dto.code) autoTagsToCreate.push(dto.code);
if (dto.type_key) autoTagsToCreate.push(dto.type_key);

for (const tagName of autoTagsToCreate) {
const slug = generateSlug(tagName);

// Find or create tag
let tag = await findTagBySlug(slug);
if (!tag) {
tag = await createTag({ name: tagName, slug });
}

// Associate with organization unit
await associateTagWithUnit(unitId, tag.id);
}

5.3 Rules

  • Tags are created in organization_unit_tags table with auto-generated slug
  • If tag with same slug exists, uses existing tag (no duplicates)
  • Tags are automatically associated via organization_unit_has_tag
  • Duplicate tags are not created (based on slug uniqueness)

6. Testing Specification

6.1 Unit Tests

  • Type hierarchy validation
  • Circular reference detection
  • Parent validation (exists, active)
  • Error code mapping
  • Auto-tag creation logic
  • Path generation (if handled in application code)

6.2 Integration Tests

  • Create deep hierarchy (≥5 levels)
  • Update parent with descendants (path recalculation)
  • Soft delete with active children
  • Hard delete after soft delete
  • Auto-tag creation on create
  • Tag association and uniqueness

6.3 Edge Case Tests

  • Concurrent child creation under same parent
  • Move node to same parent (no-op)
  • Attempt circular move (self, descendant)
  • Deactivate node with active children
  • Create with duplicate tag slugs
  • Update parent to null (become root)
  • Type hierarchy violations at different levels

6.4 Performance Tests

  • Tree listing with ≥10,000 nodes
  • Ltree path query latency (descendants, ancestors)
  • Bulk parent update operation timing
  • Search with filters (tags, type, parent)
  • Tree structure building performance

7. Developer Notes

  • All hierarchy mutations MUST be transactional
  • path_ltree column MUST have GiST index
  • Use ltree operators for hierarchical queries (e.g., pathLtree <@ parentPathLtree for descendants)
  • Prefer event-driven propagation for downstream modules (when implemented)
  • Always validate type hierarchy before create/update
  • Circular reference checks use string prefix matching on path_ltree

Path Calculation

  • Depth calculation: pathLtree.split('.').length
  • Descendant check: descendantPath.startsWith(ancestorPath)
  • Ancestor queries: Use PostgreSQL ltree operators (<@, @>, ~)

Auto-Tag Creation

  • Tags are created synchronously during unit creation
  • Tag creation is part of the same transaction
  • If tag creation fails, entire unit creation rolls back

WARNING — Row Level Security (RLS)

This project enforces RLS for tenant isolation. When writing code that accesses or mutates tenant-scoped tables:

  • Always obtain a tenant-scoped client/context before querying or mutating data; do NOT assume manual tenant_id filters are sufficient.
  • Never perform admin/cross-tenant operations without strict authorization checks; accidental use of a non-scoped client can expose or modify data across tenants.
  • For transactional work spanning multiple models, create the tenant-scoped client once and reuse it for the whole transaction to avoid context leakage.
  • Validate tenant_id from authenticated user/session and fail fast if missing — do not accept tenant identifiers from untrusted request input.

Breaking these rules can cause silent data leakage or RLS policy violations. If unsure, consult docs/RLS-USAGE-GUIDE.md.


Summary

This document defines the technical contract for the Organization Units Module. It standardizes hierarchy algorithms, error behavior, integrations, and testing expectations to ensure consistency, scalability, and safe AI-driven code generation across the system.

Key Differences from Operational Units:

  • Uses PostgreSQL ltree extension (vs materialized path)
  • Has auto-tag creation feature
  • Supports modules, information, documents, banks as related data
  • Uses type-driven hierarchy with level_order validation
  • No explicit event system (reserved for future implementation)