Skip to main content

Organization & Unit Lifecycle

Runbook for bringing a new tenant online and decommissioning one.

Onboarding a new organization

POST /api/v1/organizations
{
"name": "Acme Health",
"slug": "acme-health",
"timezone": "Asia/Kolkata",
"settings": { "fy_start_month": 4 }
}

The controller delegates to OrganizationBootstrapService::bootstrap() which — inside the same transaction as the org create — opens the "current" FinancialYear (April–March by default, configurable via settings.fy_start_month) and seeds the LeaveType catalog (casual, sick, earned, unpaid) with common-market quotas.

What it explicitly does NOT seed:

  • Holidays — upload per-region via POST /holidays or batch CSV.
  • Red-flag policies — configure per-unit via POST /red-flag-policies after the units are created.
  • Shifts — each industry's pattern is different; create them per-unit via POST /shifts.

Adding the first unit

POST /api/v1/units
{
"organization_id": 1,
"name": "Head office",
"code": "HO",
"timezone": "Asia/Kolkata",
"geo_enabled": false,
"device_binding": false
}

The request validator rejects (with 422 on organization_id) any attempt to attach a unit to a retired organization.

Decommissioning a unit

Use the retire cascade, not DELETE /units/{id}:

POST /api/v1/units/{id}/retire
{
"effective_date": "2026-04-30",
"reason": "relocation",
"transfer": [
{ "employee_id": 101, "target_unit_id": 5 },
{ "employee_id": 102, "target_unit_id": 5 }
]
}

In a single transaction RetireOrganizationUnitService:

  1. Flips the unit status to decommissioning.
  2. Moves each transfer[] employee's primary deployment — closes the old one with ends_on = effective_date and opens a new primary at the target unit with starts_on = effective_date + 1 day.
  3. Closes every other open EmployeeDeployment at the unit by stamping ends_on.
  4. Cancels every future planned ShiftAssignment at the unit.
  5. Flips the unit to retired, stamps retired_at + retired_by, flips is_active=false.

Guard: the service refuses to retire an org's last active unit — that would strand the tenant. Retire the organization itself if that's what you want.

Decommissioning an organization

POST /api/v1/organizations/{id}/retire
{
"effective_date": "2026-04-30",
"reason": "tenant sunset"
}

RetireOrganizationService:

  1. Flips org status to decommissioning.
  2. Iterates every active unit and calls the unit-retire service on it, except the last one which is inlined to bypass the "last-active-unit" guard.
  3. Flips org to retired with the same stamps.

Employees are NOT auto-offboarded — employment is a separate contract question. If the tenant is genuinely done, run POST /employees/{id}/offboard in a loop afterwards.

The status state-machine

Both organizations and organization_units carry:

ColumnMeaning
statusactive, decommissioning, retired
retired_atTimestamp when status hit retired
retired_byFK → users.id; nullOnDelete so the row
survives if the admin leaves

deleted_at (SoftDeletes) is reserved for accidents — use hard delete only for rows created in error that have no downstream history. Retirement is the normal decommissioning path.

Undoing a retirement

Only a system administrator can undo a retirement, directly at the DB:

UPDATE organizations
SET status = 'active',
retired_at = NULL,
retired_by = NULL,
is_active = 1
WHERE id = ?;

Child rows (units, deployments, assignments) are NOT auto-reopened — treat reinstatement as "start from scratch and manually re-grant the bits you need".