Skip to main content

Validation Rules

πŸ“‹ Overview​

Dokumentasi lengkap untuk validation rules yang diterapkan pada Organization Unit API endpoints.


πŸ” Validation Rules​

CREATE Organization Unit​

Required Fields​

  • name (string) - Organization unit name
  • type_key (string) - Must exist in core_organization_unit_types table
  • is_active (boolean) - Active status

Optional Fields​

  • parent_id (UUID) - Parent organization unit
  • short_name (string) - Short name or abbreviation
  • slug (string) - URL-friendly slug (auto-generated if not provided)
  • code (string) - Organization unit code
  • attributes (object) - Custom JSON attributes
  • information (object) - Contact information
  • modules (array) - Module availability
  • owners (array) - Employee owners
  • tag_ids (array) - Tag UUIDs
  • documents (array) - Document attachments
  • bank_ids (array) - Company bank UUIDs

Business Rules​

1. Parent Validation​

Rule: If parent_id is provided, the parent must exist and be active.

Validations:

  • Parent UUID must exist in database
  • Parent is_active must be true

Error Codes:

// Parent not found
{
"statusCode": 404,
"message": "Parent organization unit not found",
"reason": "organization-unit.parent-not-found"
}

// Parent inactive
{
"statusCode": 400,
"message": "Parent organization unit is inactive",
"reason": "organization-unit.parent-inactive"
}
2. Type Validation​

Rule: type_key must exist in core_organization_unit_types table.

Error Code:

{
"statusCode": 404,
"message": "Organization unit type not found",
"reason": "organization-unit.type-not-found"
}
3. Hierarchy Validation​

Rule: Child type level must be higher than parent type level.

Example Hierarchy:

Company (level 1)
└─ Branch (level 2)
└─ Department (level 3)
└─ Team (level 4)

Valid Examples:

  • βœ… Parent: Company (level 1) β†’ Child: Branch (level 2)
  • βœ… Parent: Branch (level 2) β†’ Child: Department (level 3)
  • βœ… Parent: null β†’ Child: Company (level 1) - root level

Invalid Examples:

  • ❌ Parent: Department (level 3) β†’ Child: Branch (level 2)
  • ❌ Parent: Branch (level 2) β†’ Child: Branch (level 2) - same level

Error Code:

{
"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
}
}

UPDATE Organization Unit​

All Fields Optional​

When updating, all fields are optional (partial update). Only provided fields will be updated.

Additional Validation Rules​

1. Existence Check​

Rule: Organization unit must exist before update.

Error Code:

{
"statusCode": 404,
"message": "Organization unit not found",
"reason": "organization-unit.not-found"
}
2. Circular Reference - Self​

Rule: Cannot set unit as its own parent.

Invalid Example:

{
"parent_id": "same-as-current-unit-id"
}

Error Code:

{
"statusCode": 400,
"message": "Organization unit cannot be its own parent",
"reason": "organization-unit.circular-reference-self"
}
3. Circular Reference - Descendant​

Rule: Cannot set parent to a descendant unit (prevents circular hierarchy).

How it works: Uses PostgreSQL ltree path to check if new parent is a descendant.

Invalid Example:

Current hierarchy: A β†’ B β†’ C
Trying to update: C.parent_id = A ❌ (A is ancestor of C)

Valid Example:

Current hierarchy: A β†’ B β†’ C
Trying to update: C.parent_id = D βœ… (D is not related)

Error Code:

{
"statusCode": 400,
"message": "Cannot set parent to a descendant organization unit",
"reason": "organization-unit.circular-reference-descendant"
}
4. Hierarchy Re-validation​

Rule: When changing parent_id or type_key, hierarchy rules are re-validated.

  • If only parent_id changes: Use new parent + existing type
  • If only type_key changes: Use existing parent + new type
  • If both change: Use new parent + new type
  • Same hierarchy validation as CREATE applies

DELETE Organization Unit (Soft Delete)​

Validation Rules​

1. Has Active Children​

Rule: Cannot delete a unit that still has active children.

What to do:

  1. Delete/deactivate all children first, OR
  2. Reassign children to different parent

Error Code:

{
"statusCode": 400,
"message": "Cannot delete organization unit with active children",
"reason": "organization-unit.has-active-children"
}
2. Soft Delete Behavior​
  • Sets is_active = false
  • Data is preserved (not hard deleted)
  • Can be reactivated by setting is_active = true via update

🎯 Validation Flow Diagrams​

CREATE Flow​

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Validate DTO β”‚ (class-validator)
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ validateCreate()β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Has parent_id? β”‚
β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”˜
β”‚ Yes β”‚ No
↓ ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚validateParentβ”‚ β”‚
β”‚- Exists? β”‚ β”‚
β”‚- Active? β”‚ β”‚
β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
↓ ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ validateType β”‚
β”‚ - Exists? β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚Has parent & type? β”‚
β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”˜
β”‚ Yes β”‚ No
↓ ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β”‚
β”‚validateHierarchyβ”‚β”‚
β”‚Type level > β”‚β”‚
β”‚Parent level? β”‚β”‚
β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β”‚
↓ ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ CREATE UNIT β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

UPDATE Flow​

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Validate DTO β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚validateUpdate() β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚validateExistenceβ”‚
β”‚- Unit exists? β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚parent_id changed? β”‚
β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”˜
β”‚ Yes β”‚ No
↓ ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚validateParentβ”‚ β”‚
β”‚validateCircularβ”‚ β”‚
β”‚- Not self? β”‚ β”‚
β”‚- Not descendant?β”‚ β”‚
β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
↓ ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Validate Hierarchy β”‚
β”‚ (with new/old data) β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ UPDATE UNIT β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

DELETE Flow​

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚validateDelete() β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ hasChildren() β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚Has active kids?β”‚
β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”˜
β”‚ Yes β”‚ No
↓ ↓
β”Œβ”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚REJECTβ”‚ β”‚SOFT DEL β”‚
β”‚ 400 β”‚ β”‚is_activeβ”‚
β””β”€β”€β”€β”€β”€β”€β”˜ β”‚= false β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸ“Š Error Code Reference​

Complete Error Codes List​

HTTPReason CodeTriggerSolution
404organization-unit.not-foundUnit ID not foundCheck unit ID exists
404organization-unit.parent-not-foundParent ID not foundCheck parent ID exists
400organization-unit.parent-inactiveParent is inactiveUse active parent or set parent to null
404organization-unit.type-not-foundType key not foundCheck type exists in core_organization_unit_types
400organization-unit.type-hierarchy-invalidType level ≀ parent levelUse higher type level than parent
400organization-unit.circular-reference-selfparent_id = current unit IDChoose different parent
400organization-unit.circular-reference-descendantparent is descendantChoose parent that is not in subtree
400organization-unit.has-active-childrenDeleting unit with childrenDelete/reassign children first

πŸ§ͺ Testing Scenarios​

Test Case: CREATE Success​

POST /organization-units
{
"name": "Jakarta Branch",
"type_key": "branch",
"parent_id": "company-uuid",
"is_active": true
}

// Assuming:
// - company-uuid exists and is active
// - company type level = 1
// - branch type level = 2
// Result: βœ… Success (2 > 1)

Test Case: CREATE Hierarchy Error​

POST /organization-units
{
"name": "Head Office",
"type_key": "company",
"parent_id": "branch-uuid",
"is_active": true
}

// Assuming:
// - branch type level = 2
// - company type level = 1
// Result: ❌ 400 type-hierarchy-invalid (1 ≀ 2)

Test Case: UPDATE Circular Reference​

// Current: Company A β†’ Branch B β†’ Dept C
PATCH /organization-units/company-a-uuid
{
"parent_id": "dept-c-uuid"
}

// Result: ❌ 400 circular-reference-descendant
// (Dept C is descendant of Company A)

Test Case: DELETE with Children​

// Current: Branch A has 3 active departments
DELETE /organization-units/branch-a-uuid

// Result: ❌ 400 has-active-children

πŸ’‘ Best Practices​

For API Consumers​

  1. Always validate hierarchy before submit

    • Check parent type level < child type level
    • Use GET /core/organization-unit-types to get type levels
  2. Handle error codes properly

    • Use reason field for i18n/translation
    • Show user-friendly messages based on reason code
  3. Pre-check before update parent

    • Verify new parent is not self
    • Verify new parent is not in unit's subtree
  4. Bulk operations

    • Create from top-down (parent first, then children)
    • Delete from bottom-up (children first, then parent)

For Frontend Developers​

// Example: Pre-validate before submit
function canSetParent(unitId: string, newParentId: string, units: Unit[]) {
// Check 1: Not self
if (unitId === newParentId) return false;

// Check 2: Not descendant (check if newParent is in unit's subtree)
const unit = units.find((u) => u.id === unitId);
const newParent = units.find((u) => u.id === newParentId);

if (newParent.path_ltree.startsWith(unit.path_ltree)) {
return false; // newParent is descendant
}

return true;
}

// Example: Handle validation errors
const handleError = (error) => {
const { reason, message, details } = error.response.data;

switch (reason) {
case 'organization-unit.type-hierarchy-invalid':
showError(
`Type level must be higher than parent. ` +
`Parent level: ${details.parentTypeLevel}, ` +
`Current level: ${details.currentTypeLevel}`,
);
break;

case 'organization-unit.circular-reference-descendant':
showError('Cannot set parent to a child unit');
break;

default:
showError(message);
}
};