afterhours-shift-manager/tests/weekly_post/test_pay_math.py
Adam Moussa 470e00affb
Some checks are pending
Deploy API / Deploy API to dev (push) Waiting to run
Deploy API / Deploy API to prod (push) Waiting to run
feat(schedule): align the work week with Sunday-Saturday payroll (DEV-300) (#282)
* feat(schedule): align the work week with Sunday-Saturday payroll

Saturday night stays in the week that ends Saturday, and the first Flex close skips dates already sent.

* fix(slack-bot): show the last pay close on Sunday

The Monday 7am row for the week that just ended is not written yet, so /oncall pay now falls back to the prior close.
2026-09-25 17:56:46 +00:00

236 lines
10 KiB
Python

"""Tests for weekly-post pay calculation helpers."""
from datetime import datetime
from decimal import Decimal
# 2026-06-01 is a Monday. Day names come from the date, so a week may start any day.
WEEK_START = datetime(2026, 6, 1)
# Sunday that opens the week containing Monday 2026-06-01.
SUNDAY_WEEK = datetime(2026, 5, 31)
class TestAddShiftToPay:
def test_assigned_shift_is_added(self, weeklypost_app, schedule, seed):
seed.config(shift_rate="50")
seed.weekly("Monday", "114", "Alice")
breakdown, totals = [], {}
weeklypost_app._add_shift_to_pay(
schedule, breakdown, totals, datetime(2026, 6, 1), "Monday"
)
assert len(breakdown) == 1
assert breakdown[0]["name"] == "Alice"
assert breakdown[0]["rate"] == Decimal("50")
assert totals["Alice"]["shifts"] == 1
assert totals["Alice"]["total"] == Decimal("50")
def test_available_shift_is_skipped(self, weeklypost_app, schedule, seed):
seed.config(shift_rate="50")
breakdown, totals = [], {}
weeklypost_app._add_shift_to_pay(
schedule, breakdown, totals, datetime(2026, 6, 1), "Monday"
)
assert breakdown == [] and totals == {}
def test_weekend_day_label(self, weeklypost_app, schedule, seed):
seed.config(shift_rate="50")
seed.weekly("Saturday", "200", "Alice", shift_type="day")
breakdown, totals = [], {}
weeklypost_app._add_shift_to_pay(
schedule, breakdown, totals, datetime(2026, 6, 6), "Saturday", "day"
)
assert breakdown[0]["day"] == "Sat (Day)"
class TestCalculateWeeklyPay:
def test_single_weekday_shift(self, weeklypost_app, schedule, seed):
seed.config(shift_rate="50")
seed.weekly("Monday", "114", "Alice")
record = weeklypost_app._calculate_weekly_pay(schedule, WEEK_START)
assert record["week_start"] == "2026-06-01"
assert len(record["breakdown"]) == 1
assert record["totals"]["Alice"]["total"] == Decimal("50")
def test_per_employee_rate_applied(self, weeklypost_app, schedule, seed):
seed.config(shift_rate="50")
seed.roster("114", "Alice", shift_rate="75")
seed.weekly("Monday", "114", "Alice")
record = weeklypost_app._calculate_weekly_pay(schedule, WEEK_START)
assert record["totals"]["Alice"]["total"] == Decimal("75.0")
def test_weekend_counts_day_and_night(self, weeklypost_app, schedule, seed):
seed.config(shift_rate="50")
seed.weekly("Saturday", "200", "Alice", shift_type="day")
seed.weekly("Saturday", "200", "Alice", shift_type="night")
record = weeklypost_app._calculate_weekly_pay(schedule, WEEK_START)
# Alice worked both Saturday shifts → 2 shifts, $100.
assert record["totals"]["Alice"]["shifts"] == 2
assert record["totals"]["Alice"]["total"] == Decimal("100")
def test_saturday_night_stays_in_week_ending_saturday(
self, weeklypost_app, schedule, seed
):
# Sat 2026-06-06 night runs into Sunday morning and stays on Saturday.
# Sun 2026-06-07 day opens the next week. Sun 2026-05-31 day is this week.
seed.config(shift_rate="50")
seed.weekly("Saturday", "200", "Alice", shift_type="night")
seed.weekly("Sunday", "201", "Bob", shift_type="day")
record = weeklypost_app._calculate_weekly_pay(schedule, SUNDAY_WEEK)
by_date = {line["date"]: line for line in record["breakdown"]}
assert by_date["2026-06-06"]["name"] == "Alice"
assert by_date["2026-06-06"]["day"].startswith("Sat")
assert by_date["2026-05-31"]["name"] == "Bob"
assert "2026-06-07" not in by_date
def test_sunday_day_opens_the_next_week(self, weeklypost_app, schedule, seed):
seed.config(shift_rate="50")
seed.weekly("Saturday", "200", "Alice", shift_type="night")
seed.weekly("Sunday", "201", "Bob", shift_type="day")
record = weeklypost_app._calculate_weekly_pay(schedule, datetime(2026, 6, 7))
by_date = {line["date"]: line for line in record["breakdown"]}
assert by_date["2026-06-07"]["name"] == "Bob"
assert by_date["2026-06-13"]["name"] == "Alice"
assert "2026-06-06" not in by_date
def test_empty_week_has_no_breakdown(self, weeklypost_app, schedule, seed):
seed.config(shift_rate="50")
record = weeklypost_app._calculate_weekly_pay(schedule, WEEK_START)
assert record["breakdown"] == [] and record["totals"] == {}
class TestHolidayPay:
def test_single_holiday_assignee(self, weeklypost_app, schedule, seed):
# Wed 2026-06-03 holiday; one assignee at base $50 * 1.5 = $75.
seed.config(shift_rate="50")
seed.holiday(
"2026-06-03",
slots=2,
label="Founders Day",
assignees={"114": {"name": "Alice", "claimed_at": "x"}},
)
breakdown, totals = [], {}
weeklypost_app._add_shift_to_pay(
schedule, breakdown, totals, datetime(2026, 6, 3), "Wednesday", "day"
)
assert len(breakdown) == 1
line = breakdown[0]
assert line["name"] == "Alice"
assert line["is_holiday"] is True
assert line["holiday_label"] == "Founders Day"
assert line["multiplier"] == Decimal("1.5")
assert line["base_rate"] == Decimal("50")
# rate is the effective (multiplied) pay for Slack-block compatibility.
assert line["rate"] == Decimal("75.0")
assert line["amount"] == Decimal("75.0")
assert isinstance(line["multiplier"], Decimal)
assert isinstance(line["amount"], Decimal)
assert totals["Alice"]["shifts"] == 1
assert totals["Alice"]["holiday_shifts"] == 1
assert totals["Alice"]["total"] == Decimal("75.0")
def test_two_assignees_same_holiday(self, weeklypost_app, schedule, seed):
# Two slots filled on the same holiday; each paid base * 1.5.
seed.config(shift_rate="40")
seed.roster("114", "Alice", shift_rate="40")
seed.roster("200", "Bob", shift_rate="60")
seed.holiday(
"2026-06-03",
slots=2,
label="Founders Day",
assignees={
"114": {"name": "Alice", "claimed_at": "x"},
"200": {"name": "Bob", "claimed_at": "y"},
},
)
breakdown, totals = [], {}
weeklypost_app._add_shift_to_pay(
schedule, breakdown, totals, datetime(2026, 6, 3), "Wednesday", "day"
)
assert len(breakdown) == 2
assert all(line["is_holiday"] for line in breakdown)
by_name = {line["name"]: line for line in breakdown}
assert by_name["Alice"]["amount"] == Decimal("60.0") # 40 * 1.5
assert by_name["Bob"]["amount"] == Decimal("90.0") # 60 * 1.5
assert totals["Alice"]["total"] == Decimal("60.0")
assert totals["Bob"]["total"] == Decimal("90.0")
assert totals["Alice"]["holiday_shifts"] == 1
assert totals["Bob"]["holiday_shifts"] == 1
def test_custom_multiplier(self, weeklypost_app, schedule, seed):
seed.config(shift_rate="50")
seed.holiday(
"2026-06-03",
slots=1,
label="Double Day",
assignees={"114": {"name": "Alice", "claimed_at": "x"}},
multiplier=Decimal("2"),
)
breakdown, totals = [], {}
weeklypost_app._add_shift_to_pay(
schedule, breakdown, totals, datetime(2026, 6, 3), "Wednesday", "day"
)
assert breakdown[0]["multiplier"] == Decimal("2")
assert breakdown[0]["amount"] == Decimal("100")
assert totals["Alice"]["total"] == Decimal("100")
def test_empty_holiday_adds_nothing(self, weeklypost_app, schedule, seed):
seed.config(shift_rate="50")
seed.holiday("2026-06-03", slots=2, label="Empty", assignees={})
breakdown, totals = [], {}
weeklypost_app._add_shift_to_pay(
schedule, breakdown, totals, datetime(2026, 6, 3), "Wednesday", "day"
)
assert breakdown == [] and totals == {}
def test_mixed_week(self, weeklypost_app, schedule, seed):
# A week with a weekday night shift, a weekend day+night, and a holiday.
seed.config(shift_rate="50")
# Regular Monday night for Alice.
seed.weekly("Monday", "114", "Alice")
# Holiday on Wednesday (day shift) with Alice + Bob.
seed.roster("200", "Bob", shift_rate="50")
seed.holiday(
"2026-06-03",
slots=2,
label="Founders Day",
assignees={
"114": {"name": "Alice", "claimed_at": "x"},
"200": {"name": "Bob", "claimed_at": "y"},
},
)
# Saturday day + night for Bob (regular weekend).
seed.weekly("Saturday", "200", "Bob", shift_type="day")
seed.weekly("Saturday", "200", "Bob", shift_type="night")
record = weeklypost_app._calculate_weekly_pay(schedule, WEEK_START)
# Alice: Mon night $50 + Wed holiday $75 = $125 over 2 shifts.
assert record["totals"]["Alice"]["shifts"] == 2
assert record["totals"]["Alice"]["total"] == Decimal("125.0")
assert record["totals"]["Alice"]["holiday_shifts"] == 1
# Bob: Wed holiday $75 + Sat day $50 + Sat night $50 = $175 over 3 shifts.
assert record["totals"]["Bob"]["shifts"] == 3
assert record["totals"]["Bob"]["total"] == Decimal("175.0")
assert record["totals"]["Bob"]["holiday_shifts"] == 1
# Exactly two holiday breakdown lines (Alice + Bob on Wed).
holiday_lines = [b for b in record["breakdown"] if b.get("is_holiday")]
assert len(holiday_lines) == 2
assert {line["name"] for line in holiday_lines} == {"Alice", "Bob"}
def test_night_on_holiday_date_is_regular(self, weeklypost_app, schedule, seed):
# A holiday only applies to the day shift; night falls through to weekly.
seed.config(shift_rate="50")
seed.weekly("Wednesday", "114", "Alice")
seed.holiday(
"2026-06-03",
slots=1,
label="Founders Day",
assignees={"200": {"name": "Bob", "claimed_at": "y"}},
)
breakdown, totals = [], {}
weeklypost_app._add_shift_to_pay(
schedule, breakdown, totals, datetime(2026, 6, 3), "Wednesday", "night"
)
assert len(breakdown) == 1
assert breakdown[0]["name"] == "Alice"
assert breakdown[0].get("is_holiday") is None
assert breakdown[0]["amount"] == Decimal("50")