DEV Community

Chinnamsetti Satyanarayana Swamy
Chinnamsetti Satyanarayana Swamy

Posted on • Edited on

Solving Complex Overtime Thresholds: Incorporating Holiday and Leave Hours into OTL Calculations

Introduction:

Managing compliance for multi-tiered compensation policies is a major operational challenge for global enterprises. The problem intensifies when managing complex overtime (OT) and compensatory time rules across distinct employee populations—such as Non-Exempt workers, Public Safety Officers, and Exempt teams.

A common pitfall in Oracle Cloud Time and Labor (OTL) implementations occurs when Absence Management transactions (like Holiday, Annual, or Sick Leave) must factor directly into weekly overtime thresholds. Out-of-the-box rule templates often struggle to process multi-tier splits or capture absence hours within active timecard evaluation passes.

This technical guide presents an end-to-end framework. It showcases three production-grade, two-pass Time Calculation Fast Formulas that dynamically include absence hours, reduce statutory limits, and split premium allocations across custom worker groups.

Business Scenario & Policy Matrix:

Core Architectural Challenge:

Standard Fast Formulas evaluate timecards line by line. When an absence record contains no payroll mapping data, standard evaluation loops completely ignore it.

To overcome this constraint, we implement a Two-Pass Scan Strategy:

  1. Pass 1 (Pre-Scan Optimization Loop): Isolates all holiday or module absence hours prior to calculating premiums. It splits these hours across tracking buckets for Week 1 and Week 2, and dynamically shifts down the overtime thresholds (v_net_threshold = pMaxHrs - v_holiday_hrs).

  2. Pass 2 (Sequential Allocation Loop): Iterates through actual worked hours. It contrasts cumulative work totals against the newly shifted thresholds, allowing absence periods to seamlessly push worked hours into premium bands.

Step-by-Step Formula Implementation:

Formula 1: Non-Exempt Workers (Double-Pay Component Splitting)
Demonstrates dynamic threshold manipulation and simultaneously populates parallel output arrays (out_measure_ary_over and out_measure_ary_over_p) to drive multiple payroll elements on a single split entry.

/*
---------------------------------------------------------------------------------------------------------
--                      Copyright(C) Oracle
--                      All Rights Reserved
---------------------------------------------------------------------------------------------------------
--  Application     : Oracle HCM Cloud
--  Formula  Name   : ORA_OTL_NON_EXEMPT_OVERTIME_FF
--  Formula Type    : Time Calculation rule
--  Purpose         : OT Calculation for Non Exempt Employees

--  CHANGE LOG
--  Date           Author                             Version      Description
---------------------------------------------------------------------------------------------------------
    01-07-2026    Satyanarayana Swamy Chinnamsetti      1.0          Initial Version
---------------------------------------------------------------------------------------------------------
*/
/*@section - Default values for input variables and DBI Items*/
default for HWM_CTXARY_RECORD_POSITIONS is empty_text_number
default for measure                     is empty_number_number
default for PayrollTimeType             is empty_text_number

/*@section - Input variables */
inputs are HWM_CTXARY_RECORD_POSITIONS,
           measure,
           PayrollTimeType

/*@section - Local variable declarations and initializations*/
ffs_id                      = get_context(HWM_FFS_ID, 0)
rule_id                     = get_context(HWM_RULE_ID, 0)  
ffName                      = 'ORA_OTL_NON_EXEMPT_OVERTIME_FF'
hExecType               = Get_Hdr_Text(rule_id, 'RULE_EXEC_TYPE', 'CREATE')
hCreateYn               = 'N'
pMaxHrs                 = get_rvalue_number(rule_id, 'DEFINED_LIMIT', 40)
wMaAry              = HWM_CTXARY_RECORD_POSITIONS.count
nidx                = 0
wkTotalHrsTc        = 0
v_holiday_hrs_w1    = 0
v_holiday_hrs_w2    = 0
scan_idx                = 0
day_scan_c              = 0
day_process_c       = 0
rLog                        = add_rlog(ffs_id, rule_id, '>>> Enter - ' || ffName)

/*@section - Return Variable initialization*/
out_measure_ary_under   = empty_number_number
out_measure_ary_over    = empty_number_number
out_measure_ary_over_p  = empty_number_number

/*@section - Business Logic and Interface*/
if (upper(hExecType) = 'CREATE') then 
(
    hCreateYn = 'Y'
)
v_net_threshold     = pMaxHrs

while (scan_idx < wMaAry) loop
(
  scan_idx = scan_idx + 1  
    if (HWM_CTXARY_RECORD_POSITIONS[scan_idx] = 'END_DAY') then
    (
        day_scan_c = day_scan_c + 1
    )
    if (HWM_CTXARY_RECORD_POSITIONS[scan_idx] = 'DETAIL') then
    (
        if (NOT PayrollTimeType.exists(scan_idx) and measure.exists(scan_idx)) then
        (
            if (day_scan_c < 7) then
            (
                v_holiday_hrs_w1 = v_holiday_hrs_w1 + measure[scan_idx]
            )
            else
            (
                v_holiday_hrs_w2 = v_holiday_hrs_w2 + measure[scan_idx]
            )
        )
    )
)
v_net_threshold = pMaxHrs - v_holiday_hrs_w1

rLog = add_rlog(ffs_id, rule_id, 'W1 Holiday Hrs: ' || to_char(v_holiday_hrs_w1) || ' | W1 Net Threshold: ' || to_char(v_net_threshold))

while (nidx < wMaAry) loop
(     
  nidx = nidx + 1     
  tcRecPosition = HWM_CTXARY_RECORD_POSITIONS[nidx]  
  if (tcRecPosition = 'END_DAY') then
  (
        day_process_c = day_process_c + 1  
        if (day_process_c = 7) then
        (
            wkTotalHrsTc    = 0 
            v_net_threshold = pMaxHrs - v_holiday_hrs_w2

            rLog = add_rlog(ffs_id, rule_id, '--- Switch to Week 2 --- W2 Holiday Hrs: ' || to_char(v_holiday_hrs_w2) || ' | W2 Net Threshold: ' || to_char(v_net_threshold))
    )
  )  
  if (tcRecPosition = 'DETAIL') then
  (
        if (measure.exists(nidx)) then
        (
            tcMeasure = measure[nidx]
            if (PayrollTimeType.exists(nidx)) then
            (
                if (wkTotalHrsTc >= v_net_threshold) then
                (
                    out_measure_ary_under[nidx]  = 0            
                    out_measure_ary_over[nidx]   = tcMeasure
                    out_measure_ary_over_p[nidx] = tcMeasure
                )
                else if ((wkTotalHrsTc + tcMeasure) > v_net_threshold) then
                (
                    v_reg_allowed                            = v_net_threshold - wkTotalHrsTc
                    v_ot_split                                   = tcMeasure - v_reg_allowed
                    out_measure_ary_under[nidx]  = v_reg_allowed
                    out_measure_ary_over[nidx]   = v_ot_split
                    out_measure_ary_over_p[nidx] = v_ot_split
                )
                else
                (
                    out_measure_ary_under[nidx]  = tcMeasure
                )
                wkTotalHrsTc = wkTotalHrsTc + tcMeasure
            )
        )
  )
)  
rLog = add_rlog(ffs_id, rule_id, '<< Exit - ' || ffName)

/*@section - Return the calculated value*/
return out_measure_ary_under, out_measure_ary_over, out_measure_ary_over_p
Enter fullscreen mode Exit fullscreen mode

Formula 2: Public Safety Personnel (In-Line Factor Scaling)
Directly scales the calculated remainder array by a 1.5x factor in the script's calculation loop. It handles the ELSE branch cleanly to preserve and return unmapped holiday records to the payroll engine.

/*
---------------------------------------------------------------------------------------------------------
--                      Copyright(C) Oracle
--                      All Rights Reserved
---------------------------------------------------------------------------------------------------------
--  Application     : Oracle HCM Cloud
--  Formula  Name   : ORA_OTL_PUBLIC_SAFETY_OVERTIME_FF
--  Formula Type    : Time Calculation rule
--  Purpose         : Compensatory Time Earned Calculation with Holiday Threshold Reductions

--  CHANGE LOG
--  Date           Author                             Version      Description
---------------------------------------------------------------------------------------------------------
    02-07-2026    Satyanarayana Swamy Chinnamsetti      1.0          Initial Version
---------------------------------------------------------------------------------------------------------
*/
/*@section - Default values for input variables and DBI Items*/
default for HWM_CTXARY_RECORD_POSITIONS is empty_text_number  
default for measure                     is empty_number_number 
default for PayrollTimeType             is empty_text_number 

/*@section - Input variables */
inputs are HWM_CTXARY_RECORD_POSITIONS,
                     measure, 
                     PayrollTimeType

/*@section - Local variable declarations and initializations*/
ffName            = 'ORA_OTL_PUBLIC_SAFETY_OVERTIME_FF'
ffs_id            = GET_CONTEXT(HWM_FFS_ID, 0)
rule_id           = GET_CONTEXT(HWM_RULE_ID, 0)
pMaxHrs           = get_rvalue_number(rule_id, 'DEFINED_LIMIT', 40) 
wMaAry            = HWM_CTXARY_RECORD_POSITIONS.count
scan_idx          = 0
nidx              = 0
day_scan_c        = 0
day_process_c     = 0
v_holiday_hrs_w1  = 0
v_holiday_hrs_w2  = 0
wkTotalHrsTc      = 0   
rLog              = add_rlog(ffs_id, rule_id, '>>> Enter - ' || ffName)

/*@section - Return Variable initialization*/
measure_under = empty_number_number 
measure_over  = empty_number_number

/*@section - Business Logic and Interface*/
while (scan_idx < wMaAry) loop
(
  scan_idx = scan_idx + 1  
  if (HWM_CTXARY_RECORD_POSITIONS[scan_idx] = 'END_DAY') then
  (
    day_scan_c = day_scan_c + 1
  )
  if (HWM_CTXARY_RECORD_POSITIONS[scan_idx] = 'DETAIL') then
  (
    if (not PayrollTimeType.exists(scan_idx) and measure.exists(scan_idx)) then
    (
      if (day_scan_c < 7) then
      (
        v_holiday_hrs_w1 = v_holiday_hrs_w1 + measure[scan_idx]
      )
      else
      (
        v_holiday_hrs_w2 = v_holiday_hrs_w2 + measure[scan_idx]
      )
    )
  )
)
v_net_threshold = pMaxHrs - v_holiday_hrs_w1

rLog = add_rlog(ffs_id, rule_id, 'W1 Holiday Hrs: ' || to_char(v_holiday_hrs_w1) || ' | W1 Net Threshold: ' || to_char(v_net_threshold))

while (nidx < wMaAry) loop
(     
  nidx                  = nidx + 1     
  tcRecPosition = HWM_CTXARY_RECORD_POSITIONS[nidx]  
  if (tcRecPosition = 'END_DAY') then
  (
    day_process_c = day_process_c + 1  
    if (day_process_c = 7) then
    (
      wkTotalHrsTc    = 0 
      v_net_threshold = pMaxHrs - v_holiday_hrs_w2
      rLog = add_rlog(ffs_id, rule_id, '--- Switch to Week 2 --- W2 Holiday Hrs: ' || to_char(v_holiday_hrs_w2) || ' | W2 Net Threshold: ' || to_char(v_net_threshold))
    )
  )
  if (tcRecPosition = 'DETAIL') then
  (
    if (measure.exists(nidx)) then
    (
      tcMeasure = measure[nidx]
      if (PayrollTimeType.exists(nidx)) then
      (
        if (wkTotalHrsTc >= v_net_threshold) then
        (
          measure_under[nidx] = 0            
          measure_over[nidx]  = tcMeasure * 1.5
        )
        else if ((wkTotalHrsTc + tcMeasure) > v_net_threshold) then
        (
          v_reg_allowed             = v_net_threshold - wkTotalHrsTc
          v_comp_split              = tcMeasure - v_reg_allowed
          measure_under[nidx]   = v_reg_allowed
          measure_over[nidx]    = v_comp_split * 1.5
        )
        else
        (
          measure_under[nidx] = tcMeasure
        )
        wkTotalHrsTc = wkTotalHrsTc + tcMeasure
      )
      else
      (
        measure_under[nidx] = tcMeasure
      )
    )
  )
)  
rLog = add_rlog(ffs_id, rule_id, '<< Exit - ' || ffName)

/*@section - Return the calculated value*/ 
return measure_under, measure_over
Enter fullscreen mode Exit fullscreen mode

Formula 3: Exempt Teams (Multi-Tier Comp Time Cascade)
Solves the challenge of progressive, multi-tier allocation cascading. This block processes chunk remnants sequentially across distinct limits: Standard regular time up to 37.5 hours, Tier 1 premium time up to 40.0 hours (1.0x), and Tier 2 premium time past 40.0 hours (1.5x).

/*
---------------------------------------------------------------------------------------------------------
--                      Copyright(C) Oracle
--                      All Rights Reserved
---------------------------------------------------------------------------------------------------------
--  Application     : Oracle HCM Cloud
--  Formula  Name   : ORA_OTL_EXEMPT_OVERTIME_FF
--  Formula Type    : Time Calculation rule
--  Purpose         : 

--  CHANGE LOG
--  Date           Author                             Version      Description
---------------------------------------------------------------------------------------------------------
    03-07-2026    Satyanarayana Swamy Chinnamsetti      1.0          Initial Version
---------------------------------------------------------------------------------------------------------
*/
/*@section - Default values for input variables and DBI Items*/
default for HWM_CTXARY_RECORD_POSITIONS is empty_text_number  
default for measure                     is empty_number_number 
default for PayrollTimeType             is empty_text_number 

/*@section - Input variables */
inputs are HWM_CTXARY_RECORD_POSITIONS,
                     measure, 
                     PayrollTimeType 

/*@section - Local variable declarations and initializations*/
ffName            = 'ORA_OTL_EXEMPT_OVERTIME_FF'
ffs_id            = GET_CONTEXT(HWM_FFS_ID, 0) 
rule_id           = GET_CONTEXT(HWM_RULE_ID, 0) 
pMaxHrs           = get_rvalue_number(rule_id, 'DEFINED_LIMIT', 37.5) 
pTier1Gap         = 2.5
wMaAry            = HWM_CTXARY_RECORD_POSITIONS.count
scan_idx          = 0
nidx              = 0
day_scan_c        = 0
day_process_c     = 0
v_holiday_hrs_w1  = 0
v_holiday_hrs_w2  = 0
wkTotalHrsTc      = 0   
rLog              = add_rlog(ffs_id, rule_id, '>>> Enter - ' || ffName)

/*@section - Return Variable initialization*/
measure_under = empty_number_number
measure_over = empty_number_number

/*@section - Business Logic and Interface*/
while (scan_idx < wMaAry) loop
(
    scan_idx = scan_idx + 1  
    if (HWM_CTXARY_RECORD_POSITIONS[scan_idx] = 'END_DAY') then
    (
        day_scan_c = day_scan_c + 1
    )
    if (HWM_CTXARY_RECORD_POSITIONS[scan_idx] = 'DETAIL') then
    (
        if (not PayrollTimeType.exists(scan_idx) and measure.exists(scan_idx)) then
        (
            if (day_scan_c < 7) then
            (
                v_holiday_hrs_w1 = v_holiday_hrs_w1 + measure[scan_idx]
            )
            else
            (
                v_holiday_hrs_w2 = v_holiday_hrs_w2 + measure[scan_idx]
            )
        )
    )
)
v_net_threshold1 = pMaxHrs - v_holiday_hrs_w1
v_net_threshold2 = v_net_threshold1 + pTier1Gap

rLog = add_rlog(ffs_id, rule_id, 'W1 Regular Limit: ' || to_char(v_net_threshold1) || ' | W1 Tier 1 Limit: ' || to_char(v_net_threshold2))

while (nidx < wMaAry) LOOP
(     
    nidx                    = nidx + 1     
    tcRecPosition = HWM_CTXARY_RECORD_POSITIONS[nidx]  
    if (tcRecPosition = 'END_DAY') then
    (
        day_process_c = day_process_c + 1  
        if (day_process_c = 7) then
        (
            wkTotalHrsTc     = 0 
            v_net_threshold1 = pMaxHrs - v_holiday_hrs_w2
            v_net_threshold2 = v_net_threshold1 + pTier1Gap
            rLog = add_rlog(ffs_id, rule_id, '--- Switch to Week 2 --- W2 Regular Limit: ' || to_char(v_net_threshold1) || ' | W2 Tier 1 Limit: ' || to_char(v_net_threshold2))
        )
    )
    if (tcRecPosition = 'DETAIL') then
    (
        if (measure.exists(nidx)) then
        (
            tcMeasure = measure[nidx]
            if (PayrollTimeType.exists(nidx)) then
            (
                v_rem         = tcMeasure
                v_reg_chunk   = 0
                v_tier1_chunk = 0
                v_tier2_chunk = 0
                if (wkTotalHrsTc < v_net_threshold1) then
                (
                    v_avail1 = v_net_threshold1 - wkTotalHrsTc
                    if (v_rem <= v_avail1) then
                    (
                        v_reg_chunk  = v_rem
                        wkTotalHrsTc = wkTotalHrsTc + v_rem
                        v_rem        = 0
                    )
                    else
                    (
                        v_reg_chunk  = v_avail1
                        wkTotalHrsTc = wkTotalHrsTc + v_avail1
                        v_rem        = v_rem - v_avail1
                    )
                )
                if (v_rem > 0 and wkTotalHrsTc >= v_net_threshold1 and wkTotalHrsTc < v_net_threshold2) then
                (
                    v_avail2 = v_net_threshold2 - wkTotalHrsTc
                    if (v_rem <= v_avail2) then
                    (
                        v_tier1_chunk = v_rem
                        wkTotalHrsTc  = wkTotalHrsTc + v_rem
                        v_rem         = 0
                    )
                    else
                    (
                        v_tier1_chunk = v_avail2
                        wkTotalHrsTc  = wkTotalHrsTc + v_avail2
                        v_rem         = v_rem - v_avail2
                    )
                )
                if (v_rem > 0 and wkTotalHrsTc >= v_net_threshold2) then
                (
                    v_tier2_chunk = v_rem
                    wkTotalHrsTc  = wkTotalHrsTc + v_rem
                    v_rem         = 0
                )
                measure_under[nidx] = v_reg_chunk
                if ((v_tier1_chunk + v_tier2_chunk) > 0) then
                (
                    measure_over[nidx] = (v_tier1_chunk * 1.0) + (v_tier2_chunk * 1.5)
                )
            )
            else
            (
                measure_under[nidx] = tcMeasure
            )
        )
    )
)  
rLog = add_rlog(ffs_id, rule_id, '<< Exit - ' || ffName)

/*@section - Return the calculated value*/ 
return measure_under, measure_over
Enter fullscreen mode Exit fullscreen mode

Key Functional Mechanics Explanations

1. Dynamic Threshold Shifting
Hardcoding 37.5 or 40-hour limits inside a calculation rule forces the engine to ignore overlapping historical balances. By executing an isolated pre-scan loop across data indexes (HWM_CTXARY_RECORD_POSITIONS), the logic shifts thresholds dynamically before distributing hours into worked categories. This approach includes absences in threshold metrics without altering the original absence records.

2. Remainder Cascading Logic
The cascading architecture implemented in Formula 3 uses an isolated container tracker (v_rem) to break down individual time entries. When an entry spans multiple limit zones, the logic carves out and counts chunks against available room profiles sequentially. This guarantees down-to-the-minute allocation accuracy for complex multi-tier plans.

3. Simultaneous Component Routing
To minimize configuration overhead, Formula 1 uses a multi-array mapping strategy. Instead of running separate rules to generate basic premium additions, the calculation engine returns out_measure_ary_over and out_measure_ary_over_p concurrently. This approach automatically routes double-pay balances to separate payroll components during the evaluation run.

Conclusion

Integrating timecard calculations across OTL, Absence Management, and Global Payroll requires a deliberate, memory-conscious approach to rule design. Using a two-pass scan architecture allows your team to enforce multi-tiered compliance rules, account for cross-module absence variances, and maintain stable processing performance across large user groups.

Top comments (0)