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_ltreecolumn 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
- Create organization unit with
parent_id = NULL - Database trigger or application logic generates
path_ltree(typically starts with first segment) - Persist new unit with generated path
Case B – Create Child Node
- Validate parent exists and is active
- Validate type hierarchy (child type level > parent type level)
- Create organization unit with
parent_idset - Database trigger or application logic generates
path_ltreeby concatenating parent path + new UUID segment - 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:
- Validate new parent is not descendant of current node (using ltree path check)
- Validate type compatibility and active status
- Validate hierarchy (new parent type level < current type level)
- Update
parent_idfield - Database trigger or application logic recalculates
path_ltreefor current unit and all descendants - 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
| Code | HTTP | Description |
|---|---|---|
organization-unit.not-found | 404 | Organization unit not found |
organization-unit.parent-not-found | 404 | Parent organization unit not found |
organization-unit.parent-inactive | 400 | Parent organization unit is inactive |
organization-unit.type-not-found | 404 | Organization unit type not found |
organization-unit.type-hierarchy-invalid | 400 | Type level must be higher than parent type level |
organization-unit.circular-reference-self | 400 | Organization unit cannot be its own parent |
organization-unit.circular-reference-descendant | 400 | Cannot set parent to a descendant organization unit |
organization-unit.has-active-children | 400 | Cannot delete organization unit with active children |
organization-unit.already-inactive | 400 | Organization unit is already inactive |
organization-unit.not-soft-deleted | 400 | Organization unit must be soft deleted first |
organization-unit.has-children | 400 | Cannot 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 Name | Trigger |
|---|---|
organization_unit.created | After successful creation |
organization_unit.updated | Attribute update |
organization_unit.moved | Parent or path change |
organization_unit.activated | is_active = true |
organization_unit.deactivated | is_active = false |
organization_unit.deleted | Soft delete |
organization_unit.hard_deleted | Hard 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)
| Module | Integration Purpose | Dependency Type |
|---|---|---|
| Employees | Assign owners to units | Foreign key |
| Core Modules | Module availability | Many-to-many |
| Tags | Categorization tags | Many-to-many |
| Documents | File attachments | One-to-many |
| Company Banks | Associated bank accounts | Many-to-many |
| Authorization | Scope & access control | Read-only |
| Reporting | Aggregation by hierarchy | Read-only |
5. Auto-Tag Creation Feature
5.1 Behavior
When creating an organization unit, the system automatically creates tags from:
- name - Organization unit name
- short_name - Short name (if different from name)
- code - Organization unit code (if provided)
- 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_tagstable 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_ltreecolumn MUST have GiST index- Use ltree operators for hierarchical queries (e.g.,
pathLtree <@ parentPathLtreefor 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_idfilters 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_idfrom 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_ordervalidation - No explicit event system (reserved for future implementation)