Maica Roster Costing Calculator

System documentation and architecture reference

Calculation Engine – System Documentation

This document explains how the Award Interpreter calculation engine derives rates, segments shifts, and calculates shift and roster costs. Reference data (awards, classifications, penalties and allowances) is loaded from the database — see the Reference Data tab to browse those tables.

1. Data Relationships

Award (MA000004) ├── Classifications │ ├── Retail Employee Level 1 (CA) → baseRate: $1008.90/week, calculatedRate: $26.55/hour │ ├── Retail Employee Level 1 (FT) → baseRate: $1008.90/week, calculatedRate: $21.24/hour │ └── Retail Employee Level 2 (CA) → ... │ ├── Penalties (matched by award + classification + rate type) — rates after validation (MA000004 CA Level 1) │ ├── "Saturday - ordinary hours" → $33.19/hour [flat ×1.25 on $26.55] │ ├── "Sunday - ordinary hours" → $39.83/hour [×1.50 on $26.55] │ └── "Public holiday" → $59.74/hour [×2.25 on $26.55] │ └── Allowances ├── Wage: "Shift allowance - afternoon", "Leading hand allowance", ... └── Expense: "Meal allowance", "Travel allowance - per km", ...

Note: Raw penaltyCalculatedValue figures from the database may differ from the above. The multiplier validation layer (Section 2.3.1) detects and overrides out-of-range values. For MA000004 CA Level 1, tiered Saturday rows are discarded — see Section 2.4.

1.1 Classification Selection

When a user selects:

  • Award: Filters to that award's data
  • Employment Type: Filters classifications by employeeRateTypeCode (CA = Casual, FT = Full-time, PT = Part-time)
  • Classification: Selects the specific classification row

1.2 Penalty Matching

STEP 4 Fix: A penalty row applies if:

  1. penalty.awardCode === selectedAwardCode
  2. Classification Matching (Tightened Logic):
    • For non-AD penalties: BOTH penalty.classification AND penalty.classificationLevel must match the selected classification
    • For AD (applies to all) penalties: penalty.classification matches OR penalty.classificationLevel matches

    This prevents cross-contamination where Level 2 penalties incorrectly match Level 1 queries.

  3. penalty.employeeRateTypeCode matches selected rate type OR is "AD" (applies to all)
Example:

Selection: Award MA000004, Casual (CA), Retail Employee Level 1

Matching Penalties:

  • ✓ "Saturday - ordinary hours" (CA, Retail Employee Level 1)
  • ✓ "Sunday - ordinary hours" (AD, Retail Employee Level 1)
  • ✗ "Overtime - Monday to Saturday" (FT, Retail Employee Level 1) - wrong rate type

1.3 Engine inputs

The shift/roster cost engine uses the following inputs. All of these affect the final cost.

SourceInputDescription
User (UI)AwardSelected award code (e.g. MA000004)
Employment typeSelected rate type (e.g. CA = Casual, FT, PT). Drives classification lookup and whether casual loading applies.
ClassificationSelected classification name (e.g. Retail Employee Level 1)
Casual loading %Number 0–100. Default 25. Persisted in localStorage. When user selects Casual and classification has weekly base, this is used to derive the ordinary hourly rate. 0% is valid (no loading).
Per shift: DateShift start date (YYYY-MM-DD)
Per shift: Start timeShift start time (HH:mm, 24h)
Per shift: Duration (hrs)Total shift length in hours
Per shift: Break (min)Unpaid break minutes; deducted from paid hours
Per shift: KmsKilometres (for per-km expense allowances)
Wage / expense allowancesSelected allowance types from the award
Data (database + selection)Classification rowbaseRate, baseRateType, calculatedRate, calculatedRateType, employeeRateTypeCode, etc.
Penalty rowsAll penalties matching award + classification + level + rate type (see 1.2)
Public holidaysOptional list of YYYY-MM-DD dates (e.g. from state/territory)

1.4 Calculation flow overview

High-level order of operations for a single shift:

  1. Resolve ordinary hourly rate — From classification and (when applicable) Casual loading % (see Section 2.1).
  2. Build penalty rate map — Map penalty keys (ordinary, saturday, sunday, etc.) to dollar rates; apply multiplier validation and MA000004 Saturday fix; when using casual loading for rate, set Sunday to ordinary × 1.50.
  3. Segment the shift — Split shift into 6-minute segments by day type (public holiday, Sunday, Saturday, weekday) and time-of-day (before 7am, 7am–6pm, after 6pm; Friday late). Apply overtime after 9 ordinary hours.
  4. Apply casual minimum engagement — If casual and paid hours < 3, pad to 3 hours at the shift’s day-type rate.
  5. Sum segment costs — For each segment: hours × rate from penalty map.
  6. Add wage and expense allowances — Using same effective hourly rate for percentage-based allowances.
  7. Return segments, allowances, total cost, total hours, and warnings.

2. Rate Calculation Logic

2.0 Casual loading and “use loading for rate”

The UI exposes a Casual loading % field (0–100, default 25, persisted). The engine uses it only when both of the following are true:

  • The user has selected Employment type: Casual (CA) (from the dropdown).
  • The selected classification has a weekly base rate (baseRateType = "Weekly" and baseRate present).

When both are true, the engine treats this as “use casual loading for rate”: the ordinary hourly rate is derived from the weekly base plus the user’s loading %, and the same rate is used consistently for the shift (and for percentage-based allowances). When the user has set 0%, the multiplier is 1.00 (no loading). Default 25% is used only when the value is missing or not a number.

loadingMultiplier = 1 + (casualLoadingPercent / 100)   // 0% → 1.00, 25% → 1.25
ordinaryHourly = (baseRate ÷ 38) × loadingMultiplier    // when "use loading for rate"

The “Calculated rate” shown in the Base and penalty rates panel uses this same formula when Casual is selected and the classification has a weekly base, so the displayed rate matches the rate used in the roster cost breakdown.

2.1 Ordinary hourly rate derivation

The system derives the ordinary hourly rate used for the shift in this order:

Step 1 (use casual loading for rate): If the UI has set “use loading for rate” (user selected Casual and classification has weekly base), then:
ordinaryHourly = (baseRate ÷ 38) × (1 + casualLoadingPercent/100)
This overrides the database calculated rate so the user’s Casual loading % is always applied when applicable. 0% gives base/38.
Step 2 (database hourly rate): If calculatedRateType is "Hourly" and calculatedRate exists, use calculatedRate.
Step 3 (weekly fallback): If baseRateType is "Weekly" and baseRate exists:
For Casual (CA): ordinaryHourly = (baseRate ÷ 38) × loadingMultiplier
For Full-time/Part-time: ordinaryHourly = baseRate ÷ 38
Step 4: Otherwise use calculatedRate if available.
Example – Casual, weekly base, 25% loading:
  • baseRate: $1008.90/week
  • Casual loading %: 25
  • Result: ordinaryHourly = (1008.90 ÷ 38) × 1.25 = $33.19/hour (used for shift and for percentage allowances)
Example – Casual, weekly base, 0% loading:
  • baseRate: $1008.90/week
  • Casual loading %: 0
  • Result: ordinaryHourly = (1008.90 ÷ 38) × 1.00 = $26.55/hour

2.2 Penalty Rate Mapping

Penalty descriptions are normalized to internal keys:

Penalty Description Internal Key Notes
"Saturday - ordinary hours" saturday_ordinary Saturday-specific ordinary rate
"Saturday - first 3 hours" saturday_first_3 First X hours of work on Saturday
"Saturday - after 3 hours" saturday_after_3 Hours after first X hours on Saturday
"Sunday - ordinary hours" sunday All Sunday hours
"Public holiday" publicholiday Public holiday hours
"Monday - ordinary hours" ordinary Weekday ordinary hours (Mon-Fri)

2.3 Penalty Rate Value

For each penalty, the system prefers:

  1. penaltyCalculatedValue (pre-calculated hourly rate in dollars)
  2. If percentage: ordinaryHourlyRate × (rate ÷ 100)
  3. If dollar amount: use rate directly

2.3.1 Multiplier Validation Layer (Data Quality Protection)

STEP 2 Implementation: For MA000004 Casual Level 1 employees, the system includes a multiplier validation layer that protects against incorrect database data:

  • Expected Multiplier Ranges: Each penalty key has an expected multiplier range:
    Penalty KeyExpected MultiplierRange
    ordinary×1.000.98 - 1.02
    weekday_early_late×1.101.08 - 1.12
    friday_late×1.151.13 - 1.17
    saturday / saturday_ordinary×1.251.23 - 1.27
    sunday×1.501.48 - 1.52
    publicholiday×2.252.23 - 2.27
  • Validation Process:
    1. Calculate implied multiplier: impliedMultiplier = penaltyCalculatedValue ÷ ordinaryHourlyRate
    2. Compare against expected range for that penalty key
    3. If outside range (±0.02 tolerance): Log DATA QUALITY WARNING and override with calculated rate
    4. Override rate: ordinaryHourlyRate × expectedMultiplier
  • Warning Messages: When an override is applied, a warning is added to the API response:
    "Rate override applied for [key]: database value $X.XX implies ×Y.YY which is outside expected range (min-max). Using calculated rate $Z.ZZ instead. Source row: [description] ([rateType], Level [level])"
  • Rationale: This validation layer ensures that incorrect penaltyCalculatedValue figures from the database (or penalties matched from wrong classifications) do not produce incorrect shift costs. The system automatically corrects data quality issues while alerting users to the problem.

2.4 Saturday Penalty Verification (BUG 4 Fix)

The system checks whether tiered Saturday rates exist in the database and whether they are legitimate for the selected award/classification/rate type:

  • MA000004 CA Level 1 (known exception): Tiered Saturday rows (saturday_first_X, saturday_after_X) are always discarded for this classification regardless of what the database contains. This is a confirmed data quality issue — the tiered rows in the database do not represent the correct award entitlement for this classification. The flat Saturday rate (×1.25 on ordinary hourly rate) is always used for all Saturday hours.
  • Other awards/classifications: If tiered rates exist and their implied multipliers pass the validation check in Section 2.3.1, tiered logic is applied. If only a flat rate exists (saturday_ordinary or generic saturday), the flat rate is used for all Saturday hours.
  • Consistency check: If the Saturday ordinary rate exceeds the Sunday rate after validation, a warning is generated.

2.5 Sunday rate and validation (BUG 5 Fix)

When the engine is using casual loading for the ordinary rate (user selected Casual and classification has weekly base), the database Sunday rate is often based on the unloaded rate (e.g. 26.55 × 1.50 = 39.83). The engine therefore overrides the Sunday penalty rate to:

sundayRate = ordinaryHourly × 1.50

so that Sunday costs and validation stay consistent with the loaded ordinary rate. When not using casual loading for rate, the Sunday rate from the penalty map is used as-is.

The system also validates that the Sunday rate equals ordinary × 1.50. If the value in the map (after any override) differs beyond tolerance, a validation warning is generated:

Expected Sunday rate = ordinaryHourlyRate × 1.50
Important: The "ordinary" key is set from the derived ordinary hourly rate as a fallback. If a penalty row maps to "ordinary" (e.g., "Monday - ordinary hours"), it may override this fallback.

3. Shift Cost Calculation

For each shift, the engine computes paid hours (duration minus unpaid break), splits that time into segments by day type and time-of-day, looks up the rate for each segment from the penalty map, then sums costs and adds allowances:

segmentCost = segmentHours × ratePerHour   (rate from penalty map for that segment's key)
shiftCost = Σ segmentCosts + Σ wageAllowanceCosts + Σ expenseAllowanceCosts

Standard full-time week is 38 hours (Fair Work); weekly base rates are divided by 38 to get an hourly equivalent.

3.0 Weekday Time-of-Day Penalties (BUG 1 Fix)

The system now splits weekday shifts by time boundaries and applies appropriate penalty rates:

Time Window Day Penalty Key Multiplier
Before 7:00am Mon-Fri weekday_early_late ×1.10
7:00am - 6:00pm Mon-Fri ordinary ×1.00
After 6:00pm Mon-Thu weekday_early_late ×1.10
After 6:00pm Friday friday_late ×1.15
Example:

Shift: Thursday, 5:00pm-9:00pm (4 hours)

  • 5:00pm-6:00pm: Ordinary hours (1 hr × $26.55 = $26.55)
  • 6:00pm-9:00pm: Weekday early/late (3 hrs × $29.21 = $87.63)
  • Total: $114.18

3.1 Weekday Overtime Logic (BUG 2 Fix)

After 9 ordinary hours per day, overtime multipliers apply:

  • First 3 hours of overtime: ×1.50 multiplier
  • Beyond 3 hours of overtime: ×2.00 multiplier

Important: Overtime multipliers are applied on top of whichever time-of-day penalty is active at that moment.

Example:

Shift: Monday, 10:00am-10:00pm (12 hours)

  • 10:00am-6:00pm: Ordinary hours (8 hrs × $26.55 = $212.40)
  • 6:00pm-7:00pm: Weekday early/late (1 hr × $29.21 = $29.21)
  • 7:00pm-10:00pm: Weekday early/late + overtime first 3 hrs (3 hrs × $43.81 = $131.43)
  • Total: $373.04

Note: Rate calculation = $26.55 × 1.10 (late) × 1.50 (overtime) = $43.81/hr

3.2 Casual Minimum Engagement (BUG 3 Fix)

MA000004 Clause 13.3: Casual employees must be paid a minimum of 3 hours per engagement, regardless of actual hours worked.

  • If paid hours < 3, the system pads to 3 hours
  • FIX 1: Padding hours are charged at the shift's day-type rate, not always the ordinary rate:
    • Weekday shift → padding at ordinary weekday rate
    • Saturday shift → padding at applicable Saturday rate (saturday_ordinary or saturday)
    • Sunday shift → padding at Sunday rate (×1.50)
    • Public holiday shift → padding at public holiday rate
  • A warning is added to the response: "Minimum casual engagement of 3 hours applied"
  • Applies to all day types (weekdays, weekends, public holidays)
Example - Sunday Shift:

Shift: Sunday, 2 hours worked

  • Actual hours: 2 hours
  • Minimum engagement: 3 hours
  • Padding: 1 hour at Sunday rate (×1.50 on casual loaded rate)
  • Total paid hours: 3 hours

Note: The padding hour is charged at the Sunday rate, not the ordinary weekday rate, because the shift occurs on a Sunday.

Example - Saturday Shift:

Shift: Saturday, 2 hours worked

  • Actual hours: 2 hours
  • Minimum engagement: 3 hours
  • Padding: 1 hour at the flat Saturday rate (×1.25 on ordinary hourly rate = $33.19/hr)
  • Total paid hours: 3 hours

3.3 Shift Segmentation

The system processes a shift in 6-minute increments, determining the penalty key for each moment:

For each 6-minute segment:
1. Check if it's a public holiday → publicholiday
2. Check if it's Sunday → sunday
3. Check if it's Saturday → apply Saturday logic
4. Otherwise → apply weekday time-of-day logic (see Section 3.0):
    • Before 7am = weekday_early_late (×1.10)
    • 7am–6pm = ordinary (×1.00)
    • After 6pm Mon–Thu = weekday_early_late (×1.10)
    • After 6pm Fri = friday_late (×1.15)
Saturday Logic:
1. Track hours worked in shift (excluding breaks)
2. If hours worked < X and "saturday_first_X" exists → use that
3. If "saturday_after_X" exists → use that
4. If "saturday_ordinary" exists → use that
5. Fallback to generic "saturday" or "ordinary"
Example - Saturday Shift (MA000004 CA Level 1):

Shift: Saturday, 09:00-14:00 (5 hours), 30 min break

Paid hours: 4.5 hours

Calculation:

  • All hours: 4.5 hrs at flat Saturday rate (×1.25)
  • 4.5 hrs × $33.19 = $149.36

Note: For MA000004 CA Level 1, tiered Saturday rates are not used; see Section 2.4.

3.4 Segment Accumulation

Segments with the same penalty key are accumulated:

Saturday - all hours: 4.5 hrs × $33.19 = $149.36
Total shift cost: $149.36

3.5 Break Deduction

Unpaid breaks are excluded from paid hours:

  • Break time is skipped during segmentation
  • Only worked time is charged
Example:

Shift: 8 hours duration, 30 min break

Paid hours: 7.5 hours

The 30-minute break period is not included in any segment's cost calculation.

4. Allowance Calculations

Allowances use the same effective ordinary hourly rate as the shift when they are percentage-based: that rate is passed from the shift cost engine (and includes casual loading when “use loading for rate” applies). So wage allowances stay consistent with the rate shown in the roster breakdown.

4.1 Wage Allowances

Wage allowances are calculated based on paymentFrequency and rateUnit:

Payment Frequency Calculation Example
Hourly / Per hour allowanceAmount × paidHours (or rate × paidHours) $2.50/hour × 7.5 hrs = $18.75
Per shift / Per day allowanceAmount (flat rate) $15.00 per shift
Weekly (allowanceAmount ÷ 38) × paidHours ($50/week ÷ 38) × 7.5 hrs = $9.87
Percentage (hourly) (effectiveHourlyRate × rate%) × paidHours Same hourly rate as shift (e.g. $33.19 if 25% loading)
Percentage (per shift) (effectiveHourlyRate × rate%) × 8 Same hourly rate as shift; 8 = standard shift hours

4.2 Expense Allowances

Expense allowances are calculated based on paymentFrequency:

Payment Frequency Calculation Example
Per km / Per kilometre allowanceAmount × shiftKms $0.85/km × 50 km = $42.50
Per shift / Per day / Per occasion allowanceAmount (flat rate) $12.00 per shift
For each meal allowanceAmount (assumes 1 meal per shift) $8.50 per meal

5. Roster Cost Calculation

For multiple shifts:

  1. Each shift is calculated independently using the logic above
  2. Shift costs are summed
  3. Total hours are summed
  4. Allowances are calculated per shift (not aggregated)
Example Roster:

Shift 1: Wednesday, 5 hrs → $132.75 (ordinary hours, 9am–2pm)

Shift 2: Saturday, 5 hrs → $165.94 (flat Saturday rate, 5 hrs × $33.19)

Total: 10 hrs, $298.69

6. Key Design Decisions

6.1 Saturday "First X Hours" Logic

Decision: "First X hours" refers to the first X hours worked in the shift, not hours since midnight.

Rationale: Awards typically specify "first X hours of work" on Saturday, meaning the first X hours of the employee's shift, not the first X hours of the calendar day.

6.2 Penalty Rate Precedence

Decision: Penalty rows can override the classification's ordinary rate if they map to the "ordinary" key.

Rationale: Some penalty rows (e.g., "Monday - ordinary hours") may specify rates that already include casual loading or other adjustments, so they should take precedence over the base classification rate.

6.3 Saturday Ordinary Hours

Decision: "Saturday - ordinary hours" maps to saturday_ordinary, not generic ordinary.

Rationale: Saturday has its own ordinary rate that differs from weekday ordinary rates, so it needs a separate key to be applied correctly.

7. Summary

The Award Interpreter system:

  1. Loads reference data from the database (awards, classifications, penalties, allowances)
  2. Accepts user inputs: award, employment type, classification, Casual loading % (0–100), and per-shift date, start time, duration, break, kms, and selected allowances (see Section 1.3)
  3. Matches penalties and allowances to the selected award/classification/level/rate type (Section 1.2)
  4. Derives the ordinary hourly rate: when user selected Casual and classification has weekly base, uses (base÷38)×(1 + casualLoadingPercent/100); otherwise uses database calculated rate or weekly fallback (Section 2.1)
  5. Builds the penalty rate map from matched penalties, with multiplier validation, MA000004 Saturday flat-rate fix, and when using casual loading for rate, Sunday = ordinary × 1.50 (Sections 2.2–4.5)
  6. Maps penalty descriptions to internal keys (ordinary, saturday_ordinary, sunday, etc.) for lookup
  7. Segments shifts by day type, time-of-day boundaries, and hours worked (6-minute precision)
  8. Applies weekday time-of-day penalties (before 7am, 7am–6pm, after 6pm; Friday late ×1.15)
  9. Calculates overtime multipliers (×1.50 first 3 hrs, ×2.00 beyond) on top of time-of-day penalties
  10. Enforces casual minimum engagement (3 hours) with padding at the shift’s day-type rate and warnings
  11. Validates penalty rates (Saturday/Sunday consistency, Sunday ×1.50); overrides Sunday when using casual loading for rate
  12. Calculates costs per segment using the penalty map
  13. Adds wage and expense allowances (percentage wage allowances use the same effective hourly rate as the shift)
  14. Sums all segments and allowances for total shift/roster cost
  15. Returns segments, allowances, total cost, total hours, and warnings

8. Recent Bug Fixes (February 2026)

The following bugs were identified and fixed in a code review:

  • BUG 1: Weekday time-of-day penalties now correctly applied (before 7am, 7am-6pm, after 6pm)
  • BUG 2: Weekday overtime logic implemented (max 9 ordinary hours, then ×1.50/×2.00 multipliers)
  • BUG 3: Casual minimum engagement enforced (3 hours minimum with padding at day-type rate)
  • BUG 4: Saturday penalty rate verification; MA000004 CA Level 1 uses flat ×1.25 (no tiered Saturday)
  • BUG 5: Sunday rate multiplier validation (×1.50); Sunday overridden to ordinary×1.50 when using casual loading for rate
  • BUG 6: Casual loading: user-setting “Casual loading %” (0–100, default 25) applied when user selects Casual and classification has weekly base; 0% respected (no longer treated as 25%)

Additional behaviour: “Calculated rate” in the UI reflects the same effective rate used in shift cost when Casual + weekly base; percentage-based wage allowances use that same effective hourly rate. All fixes include comprehensive unit tests in tests/shift-cost-bugs.test.js.

Documentation generated for Award Interpreter system

Last updated: February 2026