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 /holidaysor batch CSV. - Red-flag policies — configure per-unit via
POST /red-flag-policiesafter 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:
- Flips the unit status to
decommissioning. - Moves each
transfer[]employee's primary deployment — closes the old one withends_on = effective_dateand opens a new primary at the target unit withstarts_on = effective_date + 1 day. - Closes every other open
EmployeeDeploymentat the unit by stampingends_on. - Cancels every future
plannedShiftAssignmentat the unit. - Flips the unit to
retired, stampsretired_at+retired_by, flipsis_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:
- Flips org status to
decommissioning. - 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.
- Flips org to
retiredwith 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:
| Column | Meaning |
|---|---|
status | active, decommissioning, retired |
retired_at | Timestamp when status hit retired |
retired_by | FK → 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".