HRM — Leave & Holidays
Time off: what's allowed, how much is left, and who approves it
What This Module Does (Plain Language)
This module handles staff time off — annual leave, sick days, casual leave, and so on. It tracks how many days each person has, lets them apply, routes the request to a manager to approve or reject, and keeps the balance honest the whole way. It also stores a company holiday calendar so public holidays aren't counted as absences or against someone's leave balance.
The Building Blocks
| Entity | What it is | Key fields |
|---|---|---|
| Leave Type | A category of leave (Annual, Sick, Casual…) | name, code, isPaid, isBalanceTracked, allowHalfDay, requiresDocument, maxConsecutiveDays |
| Leave Policy | Entitlement & accrual rules (can vary by seniority level) | entitledDaysPerYear, accrualMethod, carryForwardMax, minTenureMonths |
| Leave Balance | Per-employee, per-type yearly tracker | entitledDays, usedDays, pendingDays, carriedDays → availableDays is derived |
| Leave Request | The application + its workflow state | startDate, endDate, isHalfDay, totalDays, reason, status |
| Holiday | A calendar entry (public/company/etc.), one day or a range | date, endDate (null = single day), type, isRecurring, storeId (null = all stores) |
availableDays = entitled + carried − used − pending. Because it is
derived, it can never drift out of sync with the real bookings.
Accrual methods & holiday types
| Enum | Values |
|---|---|
accrualMethod | ANNUAL_LUMP · MONTHLY_ACCRUAL · PER_WORKED_DAYS · NONE |
HolidayType | PUBLIC · RELIGIOUS · OPTIONAL · COMPANY |
LeaveRequestStatus | PENDING · APPROVED · REJECTED · CANCELLED · CANCELLED_POST |
How the Balance Stays Honest
POSVelo uses a reserve-on-request, deduct-on-approve model. This prevents someone from "double-spending" the same days across two pending requests.
On request
The days are reserved: they move from available to
pending. If you don't have enough available, the request is rejected
(409 INSUFFICIENT_BALANCE).
On approve
The reservation converts to used: pending → used.
On reject / cancel-before-start
The reservation is released back to available.
On cancel after approval (before it starts)
The days are refunded: used → available, and the request is
marked CANCELLED_POST.
Approved leave can be cancelled only before its start date. After the start date the leave is considered consumed and the record is final.
Every balance change happens inside a transaction that locks the balance row, so two requests submitted at the same moment can't both grab the last day.
Applying for Leave
As an employee (self-service)
Go to My Space › Leave › Apply (/me/leave/apply),
pick a type, choose dates, and submit. Track status at /me/leave.
The apply form has a Single day / Date range toggle. Single
day takes one date; Date range shows From / To with a live summary such as
“Mon, 30 Jun – Fri, 4 Jul · 5 calendar days”. Half-day is offered only for a
single-day request. The request body is unchanged — both produce a
startDate / endDate pair.
As HR (for another employee)
The HR equivalent at /hr/leave/requests takes the same body plus
an employeeId. Permission: leave-request create.
On submit, POSVelo validates there's no overlap with another request, computes
totalDays by excluding weekends and holidays,
checks maxConsecutiveDays and the document requirement, reserves
the balance (for paid, tracked leave), and notifies the approvers
(HR managers, managers, admins) with a live notification.
Approving & Rejecting
Approvers see pending requests at /hr/leave/requests.
| Action | Endpoint | Effect |
|---|---|---|
| Approve | POST /api/v2/hr/leave/requests/:id/approve | Status → APPROVED; pending days convert to used; employee notified. |
| Reject | POST /api/v2/hr/leave/requests/:id/reject | Status → REJECTED; reservation released; employee notified. |
| Cancel | POST /api/v2/hr/leave/requests/:id/cancel | Releases or refunds depending on whether it was approved yet. |
A leave request notifies every approver. When one of them approves it, the now-stale "New leave request" notification is resolved for all the others too — so nobody is left with a phantom unread badge. See Real-Time Notifications.
The Team Leave Calendar
A shared “who’s out” month view shows approved leave as a continuous bar across the days it covers, coloured by leave type, with an “Away today” / “Upcoming” side rail. It has two entry points backed by one endpoint.
| Audience | Route | Sees |
|---|---|---|
| HR / managers | /hr/leave/calendar | Whole tenant (HR/admin) or own store plus direct reports, with an all-stores filter for the all-scope permission |
| Employees | /me/leave/calendar | Their own store plus direct reports — a “Team Leave” view |
The calendar shows approved leave only. HR / admins see the whole tenant; everyone else sees their own store and direct reports. The response is a deliberately narrow projection — person, leave type, and the from–to range — and omits the reason and any attached document, so a teammate’s private note never leaks into the shared view. No new permission is needed: every ESS user already holds leave-read, and the server narrows what each role can see.
Validation Rules to Know
| Rule | Behavior |
|---|---|
| Paid vs unpaid | Only paid + balance-tracked leave touches the balance. Unpaid leave (LWP) reserves 0 days and is docked later in payroll. |
| Half-day | Allowed only on a single-day request (start = end); counts as 0.5 days. |
| Document required | If the leave type needs a document and none is attached, the request is refused. |
| Eligibility | The employee must meet the policy's minimum tenure; terminated/resigned staff can't file new requests. |
| No self-approval | If the requester is also the approver, it escalates to an admin/HR manager. |
| Carry-forward | At year rollover, unused days carry over up to the policy cap; the rest lapse. |
The Holiday Calendar
Holidays keep public/company days from being counted as absent or deducted from leave. You can add them one at a time, or import a country preset.
Dev Only POST /api/v2/hr/holidays/import-preset · permission hr.holidays.manage
Supported presets: US, UK, IN, BD, UAE. A storeId of null applies to all
stores. View the merged calendar at GET /api/v2/hr/holidays/calendar?year=&storeId=.
A holiday can be a single date or a date range — set an endDate
for a multi-day holiday such as a multi-day Eid. A null endDate
means a single day (the effective end is the start date), so existing
single-day holidays need no change. Every working day inside
the span is excluded from leave-day totals, and the holiday calendar tints the
whole range. Setting endDate equal to the start collapses it back
to a single-day holiday.
In the app
| Page | Route |
|---|---|
| Leave overview | /hr/leave |
| Requests | /hr/leave/requests |
| Balances | /hr/leave/balances |
| Policies | /hr/leave/policies |
| Team leave calendar | /hr/leave/calendar |
| Holidays | /hr/holidays |
| Holiday calendar | /hr/holidays/calendar |
Who Can Do What
| Capability | ADMIN | HR_MANAGER | MANAGER | EMPLOYEE |
|---|---|---|---|---|
| Manage leave types & policies | Yes | Yes | No | No |
| Read / adjust all balances | Yes | Yes | Team read | Own (via ESS) |
| Approve / reject requests | Yes | Yes | Team | No |
| Apply / cancel own leave | Yes | Yes | Yes | Yes (via ESS) |
| Manage holidays | Yes | Yes | No | No |
Approved paid leave and unpaid (LWP) days both feed into Payroll — set that up next.