Skip to main content

Leave & Holidays (LVE)

Module code LVE · Release R1 (MVP) · Packaging Core · Requirements FR-LVE-*, BR-LVE-*, RULE-LVE-* in the PRD and BRD · Related Attendance, Payroll, Exit, Organization (holiday calendars), Notifications & Workflow

1. Overview and scope​

LVE manages what leave an employee is entitled to, what they have taken, and what it costs payroll. It is the most-used self-service feature and the source of LOP (loss of pay) for payroll. The design centres on three ideas:

  1. A leave-type builder with templates, instead of a fixed list.
  2. Effective-dated leave policies per type, assigned by applicability rules.
  3. An append-only leave ledger. A balance is always a sum of ledger entries, never a mutable number.

In scope (R1)

  • Leave types (seeded templates: Casual, Sick, Earned/Privilege, Comp-off, Loss of Pay, Maternity, Paternity, Adoption, Marriage, Bereavement, Advance) plus custom types.
  • Leave cycle (April–March or January–December, per tenant), accrual (upfront or periodic, pro-rata on joining), carry-forward, lapse, encashment at year end and at F&F.
  • Requests with half days, sandwich rule, notice period, minimum service, document thresholds, per-instance caps, lifetime caps and gaps between terms.
  • Comp-off earned from worked week-offs and holidays (from ATT), with expiry.
  • Optional (restricted) holidays with a yearly quota.
  • Cancellation of approved leave (future-dated, or past-dated with approval).
  • Opening-balance import and manual adjustments with reasons.
  • Team calendar, conflict warnings and the leave-to-payroll bridge.

Out of scope: holiday calendar maintenance (ORG owns it), calendar sync with Google/Microsoft (R3), leave forecasting analytics (R4).

2. Learned from the reference product​

Source: Spryple analysis, §6, §16, §19 and §21.4.

  • [Observed] There are ten fixed leave types that can only be activated or deactivated; adding a type means a vendor change (SP-LVE-02; A: Leave Mgmt).
  • [Observed] Per-type rules are India-specific and rich: credit frequency (monthly, quarterly, half-yearly, yearly), sandwich flags, max per instance, minimum service months, advance notice, document required above N days, carry-forward cap, lapse to perks, comp-off minimum hours and expiry, maternity minimum gap between terms (SP-LVE-03).
  • [Observed] Common rules include sandwich across weekends and holidays, a past-dated leave window, configurable emails and an April–March or January–December cycle (SP-LVE-01).
  • [Observed] Eligibility for maternity, paternity and wedding leave depends on gender and marital status (SP-LVE-04). We drop this (see D-LVE-05).
  • [Observed] Leave policy changes apply only to new employees (SP-LVE-07; SP-X-08).
  • [Observed] Managers enter leave on behalf of employees, which is auto-approved with the balance shown; there is a cancellation approval queue and a leave "Payroll Report" with LOP and absent counts (SP-LVE-06).
  • [Inferred] Balances appear to be stored values; no ledger-style history of credits and debits is documented (§6.3).

3. Our design decisions​

IDDecision [Proposed]Reason
D-LVE-01Leave types are tenant data created from templates. A type has a category (PAID, UNPAID, COMP_OFF, STATUTORY_PARENTAL, SPECIAL) that drives payroll and defaults.Tenants need Restricted, Study, Sabbatical, Menstrual or state-specific types without a code change.
D-LVE-02Each type has one or more leave policies. A policy has effective-dated versions with a typed settings document, and is assigned by applicability (location, grade, employment type, department, designation).Different rules for factory and office staff; mid-year changes apply from a chosen date to existing employees.
D-LVE-03Leave ledger (lve_ledger_entry) is the only source of balances. A cached lve_balance row is a projection, rebuilt from the ledger.Every balance is explainable to the employee and auditor. Corrections are entries, not overwrites.
D-LVE-04Pending requests reserve balance. available = ledger_balance - pending_reserved.Prevents two requests spending the same days.
D-LVE-05Eligibility is a rule over configurable attributes (service, employment type, gender identity where the law needs it, number of prior availments). Marital status is never used. Parental leave is inclusive: birth, adoption and commissioning parents.Legal and ethical risk in the reference design; the Maternity Benefit Act does not require marriage (verify with counsel).
D-LVE-06The sandwich rule is computed by a pure function over the request, adjacent approved leave and the location's calendar. Results are stored per day in lve_request_day.Explainable day counts; recompute when a holiday changes.
D-LVE-07Approvals go through the NTF engine. Default chain: line manager; conditional HR step when days exceed a threshold.The reference product documents single-level only.
D-LVE-08Late-mark deductions from ATT post LATE_DEDUCTION entries here, in a configured type order, falling back to LOP.One balance, one history.
D-LVE-09Encashment is a request type with its own policy (max days, eligible types, formula base) that produces a payroll input.The reference product only "converts to perks".

4. Domain model​

Ledger entry_type values: OPENING, ACCRUAL, PRORATA_ACCRUAL, AVAIL (negative), AVAIL_REVERSAL, ADJUSTMENT, CARRY_FORWARD_OUT, CARRY_FORWARD_IN, LAPSE, ENCASHMENT, COMP_OFF_CREDIT, COMP_OFF_EXPIRY, LATE_DEDUCTION, ADVANCE, FNF_SETTLEMENT. Entries are never updated; a correction is a new entry that references reverses_entry_id.

lve_request_day.day_kind: WORKING, WEEK_OFF, HOLIDAY. charge is 0, 0.5 or 1.

Optional holidays: an optional holiday is a row in the ORG holiday calendar with type = OPTIONAL. Opting in is an lve_request against the system type OPTIONAL_HOLIDAY, limited by optional_holidays_per_year.

5. State machines​

5.1 Leave request​

Cancelling leave whose dates fall in a locked attendance period is allowed only with HR approval and produces an ATT adjustment for the next pay run.

5.2 Comp-off credit​

When the attendance policy sets offday_compensation = COMP_OFF and comp_off.auto_credit = true, the request step is skipped.

5.3 Leave cycle​

6. Key business logic​

6.1 Policy resolution​

function policyFor(employee, leaveType, date):
candidates = lve_policy_assignment for leaveType where applicability matches the
employee's job assignment on date (location, grade, employment type,
department, designation) and valid on date
pick the highest priority; ties fail validation at publish time
return the lve_policy_version valid on date

When a new version is published, transition_mode decides how existing employees move: FROM_DATE (new rules from valid_from, past entries untouched), RECOMPUTE_CYCLE (re-run accruals for the open cycle and post differences as ADJUSTMENT), or NEW_JOINERS_ONLY (reference-product parity, rarely used).

6.2 Accrual​

nightly job, per tenant, per employee, per active leave type:
v = policyFor(employee, type, today)
if not v.accrual.enabled: continue
if employee.service_months < v.eligibility.min_service_months_for_accrual: continue

for each credit point P due since the last accrual entry:
# P is the cycle start (UPFRONT), or the start or end of each month, quarter or
# half-year (PERIODIC), per v.accrual.frequency and v.accrual.credit_on
period_quota = v.annual_quota / periods_per_year(v.accrual.frequency)

if P is the employee's first period: # pro-rata on joining
if v.accrual.prorata_rule == DAYS_IN_PERIOD:
amount = period_quota * days_employed_in_period / days_in_period
elif v.accrual.prorata_rule == JOIN_BEFORE_DAY:
amount = period_quota if joining_day <= v.accrual.join_before_day else 0
else:
amount = period_quota

if v.accrual.reduce_for_lop: # e.g. EL earned on days worked
amount = amount * (1 - lop_days_in_period / working_days_in_period)

amount = roundTo(amount, v.accrual.rounding) # 0.5, 0.25 or 0.01
amount = min(amount, v.max_balance - currentBalance) # balance cap
if amount > 0: post ledger ACCRUAL or PRORATA_ACCRUAL(amount, effective_date = P)

# Idempotency: unique key (employee, type, entry_type, effective_date, source_type = ACCRUAL_RUN)

UPFRONT credit for a mid-cycle joiner uses amount = annual_quota * remaining_months / 12, rounded per policy.

6.3 Day counting and the sandwich rule​

function chargeDays(request, v, calendar):
days = []
for d in request.from_date .. request.to_date:
kind = HOLIDAY if calendar.isHoliday(employee.location, d)
else WEEK_OFF if ATT.isWeekOff(employee, d) else WORKING
half = halves for d (first/second/full) from from_half and to_half
charge = 0.5 if half != FULL else 1
if kind != WORKING: charge = 0 # provisional
days.append(d, kind, half, charge)

if v.sandwich.weekoffs or v.sandwich.holidays:
# Find runs of non-working days bounded by leave on both sides.
# Bounds may come from this request or, when v.sandwich.scope == ADJACENT,
# from an approved or pending leave on the immediately adjacent working day.
for each run R of consecutive non-working days:
left = leave on the working day just before R (this request or adjacent)
right = leave on the working day just after R (this request or adjacent)
if left and right
and (left is a full day or v.sandwich.half_day_counts)
and (right is a full day or v.sandwich.half_day_counts)
and (left.type in v.sandwich.bounding_types and right.type in v.sandwich.bounding_types):
for d in R:
if (d.kind == WEEK_OFF and v.sandwich.weekoffs) or (d.kind == HOLIDAY and v.sandwich.holidays):
d.charge = 1 ; d.sandwiched = true

return days, sum(charge)

Examples (Saturday and Sunday are week-offs, sandwich on for week-offs):

RequestCharged days
Fri to Mon, full days4 (Sat, Sun sandwiched)
Fri only, then a separate Mon request with scope = ADJACENTFri 1, Mon 3 (Sat and Sun charged on the second request)
Fri second half to Mon, half_day_counts = false1.5 (no sandwich)
Thu to Fri, with Sat a holiday and Sun a week-off, holidays not sandwiched2

When a holiday is added or removed after approval, LVE recomputes lve_request_day for future approved requests at that location and posts the difference as ADJUSTMENT with reason HOLIDAY_CHANGE.

6.4 Request validation​

function validate(request):
v = policyFor(employee, type, request.from_date)
check employee meets v.eligibility (service months, employment type, gender rule
when the type requires it, max_times_in_service, min_gap_between_terms_months)
check request.from_date >= today + v.notice_days unless emergency type
check request.from_date >= today - v.backdate_days past-dated window
days, charged = chargeDays(request, v, calendar)
check v.min_per_instance <= charged <= v.max_per_instance
check charged <= v.max_per_period for the month or quarter, if set
check document attached if charged > v.document_required_above_days
check no overlap with another non-cancelled request (any type)
check not combined with a type in v.cannot_combine_with on adjacent days
available = balance(employee, type) - reserved
if charged > available:
if v.allow_negative_balance up to v.negative_limit: allow and flag ADVANCE
elif v.overflow_to_lop: split the request, remainder as LOP days
else: reject "insufficient balance"
warn (not block) if team members on leave on the same dates >= v.team_conflict_threshold

6.5 Approval and ledger posting​

on submit: reserved += charged (in one transaction, row lock on lve_balance)
on approve: post AVAIL(-charged, effective_date = from_date) ; reserved -= charged
emit lve.request.approved with per-day halves for ATT
on reject or withdraw: reserved -= charged
on cancel after approval: post AVAIL_REVERSAL(+charged) ; emit lve.request.cancelled

Concurrency: approvals and submissions take SELECT ... FOR UPDATE on the employee's lve_balance row for that type and cycle. The projection updates in the same transaction as the ledger insert.

6.6 Year-end close​

for each employee, for each type at cycle_end:
bal = balance at cycle_end
carry = min(bal, v.carry_forward.max_days) # 0 when not allowed
encash = 0
if v.encashment.at_year_end:
encash = min(bal - carry, v.encashment.max_days)
lapse = bal - carry - encash
post CARRY_FORWARD_OUT(-carry) in old cycle ; CARRY_FORWARD_IN(+carry) in new cycle
if encash > 0: create lve_encashment(trigger = YEAR_END) and post ENCASHMENT(-encash)
if lapse > 0: post LAPSE(-lapse)
# Carry-forward older than v.carry_forward.expiry_months lapses on its own date.

6.7 Comp-off​

  • Eligibility comes from att.day.computed with status WP or HP and worked_minutes >= comp_off.min_minutes. Half credit applies when worked minutes are between the half and full thresholds.
  • expires_on = worked_date + comp_off.expiry_days. Availing uses credits in FIFO order of expiry.
  • A request for comp-off credit must be raised within comp_off.request_window_days, matching the reference "backdated window" [Observed SP-LVE-03].

6.8 LOP feed to payroll​

At attendance period lock, PAY asks LVE for each employee's unpaid days in the period: approved LOP leave days, plus unpaid portions of other types (e.g. negative balance beyond limit converted to LOP). These are combined with ATT absences in the payable-days calculation (see Payroll).

7. Configuration​

Per leave policy version (settings):

SettingTypeDefaultDescription
annual_quotanumber12Days per cycle
accrual.enabledbooleantrueWhether the type accrues
accrual.frequencyenumMONTHLYUPFRONT, MONTHLY, QUARTERLY, HALF_YEARLY, YEARLY
accrual.credit_onenumPERIOD_STARTPERIOD_START or PERIOD_END
accrual.prorata_ruleenumDAYS_IN_PERIODDAYS_IN_PERIOD, JOIN_BEFORE_DAY or NONE
accrual.join_before_dayinteger15Used with JOIN_BEFORE_DAY
accrual.reduce_for_lopbooleanfalseAccrual reduced by LOP days
accrual.roundingnumber0.5Rounding step for accrued days
max_balancenumbernullCap on balance; null means no cap
eligibility.min_service_monthsinteger0Service needed to apply
eligibility.employment_typesarray of stringallE.g. PERMANENT, PROBATION, CONTRACT
eligibility.gender_ruleenumANYANY, or a statutory rule where the law requires one; never marital status
eligibility.max_times_in_serviceintegernullLifetime availments (e.g. parental)
eligibility.min_gap_between_terms_monthsintegernullGap between availments
notice_daysinteger0Advance notice required
backdate_daysinteger7How far back a request may start
min_per_instancenumber0.5Smallest request
max_per_instancenumbernullLargest request
max_per_periodnumbernullPer month cap
half_day_allowedbooleantrueAllows half days
document_required_above_daysnumbernullE.g. 2 for sick leave
sandwich.weekoffsbooleanfalseCount enclosed week-offs
sandwich.holidaysbooleanfalseCount enclosed holidays
sandwich.scopeenumWITHIN_REQUESTWITHIN_REQUEST or ADJACENT
sandwich.half_day_countsbooleanfalseHalf-day bounds trigger sandwich
sandwich.bounding_typesarray of stringthis typeTypes whose leave on both sides triggers it
allow_negative_balancebooleanfalseAdvance leave
negative_limitnumber0Max negative days
overflow_to_lopbooleantrueExcess days become LOP
cannot_combine_witharray of string[]Types that may not be adjacent
carry_forward.max_daysnumber0Days carried to the next cycle
carry_forward.expiry_monthsintegernullCarried days lapse after this
encashment.at_year_endbooleanfalseEncash surplus at cycle end
encashment.max_daysnumber0Encashable days per cycle
encashment.at_exitbooleantrueEncash balance in F&F
encashment.baseenumBASIC_DABASIC_DA, GROSS or CUSTOM_FORMULA
encashment.divisorinteger26Per-day rate divisor (verify against company policy and law)
comp_off.min_minutesinteger240Minutes on an off day to earn half a day
comp_off.full_day_minutesinteger480Minutes to earn a full day
comp_off.expiry_daysinteger60Days until a credit expires
comp_off.request_window_daysinteger30Days to request a credit
comp_off.auto_creditbooleanfalseCredit without a request
team_conflict_thresholdintegernullWarn when this many teammates overlap
self_cancel_futurebooleantrueEmployee may cancel approved future leave without approval

Tenant-level settings: cycle_start_month (APRIL or JANUARY), optional_holidays_per_year (integer, default 2), notify_on (array of SUBMIT, APPROVE, REJECT, CANCEL), late_deduction_order (from ATT policy).

8. Screens by persona​

PersonaWebMobile
EmployeeBalances with ledger drill-down ("why is my balance 7.5?"), apply leave with live day count and sandwich explanation, my requests, cancel, comp-off credits with expiry, optional holidays, holiday list (next three and all), policy viewApply leave with balance chips, contact during leave, history, holiday list
Line managerTeam leave calendar, approval inbox with balance and conflicts shown, cancellation approvals, on-behalf leave and comp-off, team balancesApprovals, team calendar, on-behalf leave
HR managerAdjustments with reason, opening-balance import (dry-run), cycle close console, encashment approvals, reports—
HR adminLeave-type builder from templates, policy editor with versions and applicability preview, simulation ("what would employee X accrue under this version?")—
Payroll managerLeave-to-payroll bridge: LOP days per employee for the period, encashment inputs—

9. Events​

EventEmitted whenMain consumers
lve.request.submittedA request is submittedNTF (start workflow)
lve.request.approvedFinal approval or on-behalf auto-approvalATT (recompute days), NTF, calendar sync (R3)
lve.request.rejectedRejectedNTF
lve.request.withdrawnWithdrawn by the employeeNTF
lve.request.cancellation_requestedEmployee asks to cancel approved leaveNTF (start cancellation workflow)
lve.request.cancelledCancellation completesATT (recompute), PAY (adjustment if period locked)
lve.balance.accruedAn accrual run posts entries (batched per tenant)RPT
lve.ledger.adjustedA manual or system adjustment is postedNTF (inform employee), RPT
lve.comp_off.creditedA comp-off credit is postedNTF
lve.comp_off.expiringSeven days before expiryNTF (reminder)
lve.comp_off.expiredCredit expiredNTF
lve.cycle.closedYear-end close completesPAY (encashment inputs), RPT
lve.encashment.approvedEncashment approvedPAY (one-time input)

Consumed: att.day.computed (comp-off eligibility), att.late.threshold_breached (post LATE_DEDUCTION), org.holiday.changed (recompute future requests), emp.employee.joined (pro-rata accrual), emp.job.changed (policy re-resolution), ext.separation.approved (block leave beyond LWD), ext.fnf.initiated (encashment at exit).

10. API surface​

Detailed in API endpoints.

ResourceMain operations
/v1/lve/typesCRUD, POST /from-template
/v1/lve/policies and /v1/lve/policies/:id/versionsCRUD, publish, GET /:id/applicability-preview, POST /:id/simulate
/v1/lve/balancesGET by employee (SELF, TEAM, ORG) with as-of date
/v1/lve/ledger-entriesGET by employee and type; POST adjustment (HR)
/v1/lve/requestsPOST, GET, PATCH, POST /:id/withdraw, POST /:id/cancel; POST /quote returns the day count and sandwich breakdown without saving
/v1/lve/comp-off-creditsGET, POST request credit
/v1/lve/optional-holidaysGET available, POST opt-in (as a request)
/v1/lve/encashmentsPOST, GET
/v1/lve/cyclesGET, POST /:id/close
/v1/lve/opening-balances/importPOST with dry_run=true
/v1/lve/payroll-bridgeGET LOP and encashment per employee for a pay period (consumed by PAY)

11. Permissions​

PermissionTypical scopesDefault roles
lve.balance.readSELF, TEAM, ORGemployee, line_manager, hr_manager, auditor
lve.request.createSELFemployee
lve.request.create_on_behalfTEAM, LOCATIONline_manager, hr_manager
lve.request.approveTEAM, LOCATION, ORGline_manager, hr_manager
lve.request.cancel_approvedTEAM, ORGline_manager, hr_manager
lve.ledger.adjustORG, LOCATIONhr_manager
lve.type.manageORGhr_admin
lve.policy.manageORGhr_admin
lve.cycle.closeORGhr_admin
lve.encashment.approveORGhr_manager, payroll_manager
lve.import.runORGhr_admin
lve.report.exportTEAM, ORGline_manager, hr_manager, org_owner

12. Reports​

ReportContentsReference
Leave balanceOpening, accrued, availed, adjusted, lapsed, closing per type[Observed] SP-LVE-05, extended with ledger columns [Proposed]
Leave ledger statementEvery entry with reason and source for one employee[Proposed]
Leave registerEmployee-wise, date-wise, type-wise and designation-wise availment[Observed] SP-LVE-06
Leave summaryWorked days vs leave days by employee[Observed] SP-LVE-06
Leave payroll reportApproved leave, LOP and absent counts per pay period[Observed] SP-LVE-06
Comp-off registerEarned, used, expired, expiring soon[Observed] SP-LVE-06
Encashment reportDays and amounts by trigger[Proposed]
Absenteeism and leave trendLeave rate by department and month, Bradford-style frequency[Proposed] R4
Statutory leave registerFormat required by the state Shops & Establishments rules[Proposed] R2, verify per state

13. Release and priority​

CapabilityReleaseMoSCoW
Leave-type builder with templates; policies with versions and applicabilityR1Must
Ledger, accrual (upfront and periodic, pro-rata), carry-forward, lapseR1Must
Requests with half days, sandwich, notice, documents, caps; cancellationR1Must
On-behalf leave, multi-level approval through NTFR1Must
Comp-off from ATT with expiry; optional holidaysR1Must
Late deduction from ATT; LOP bridge to PAYR1Must
Opening-balance importR1Must
Year-end encashment; encashment at exitR1 (year end), R2 (exit, with EXT)Should
Policy simulation toolR1Should
Team conflict warningsR1Should
Calendar sync (Google/Microsoft)R3Could
Statutory leave registers per stateR2Should
Leave analyticsR4Could

14. Open questions​

  1. Does sandwich across adjacent requests apply when the first request is already approved and the second is pending? The default is yes, charged on the second request.
  2. Should holiday changes after approval re-charge already-availed (past) leave? The default is future requests only.
  3. Which state-specific statutory leave minimums should we ship as templates in R1 (e.g. Karnataka, Maharashtra, Telangana, Tamil Nadu)? Verify each with counsel.
  4. Do customers want hourly leave units (as opposed to ATT permissions)? unit exists on the type; R1 supports DAY only.
  5. How should encashment be taxed and reported? It belongs in PAY as a component with its own tax treatment; confirm exemption rules at exit.